From 9d564faa6ab404253cf13703698a909a680ea7e2 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Mon, 10 Aug 2026 17:03:38 +1000 Subject: [PATCH 01/24] feat: add Agent Attention playground experiment --- .agents/plugins/marketplace.json | 6 +- .claude-plugin/marketplace.json | 16 +- .github/.release-please-manifest.json | 2 +- .github/release-please-config.json | 74 +- CHANGELOG.md | 68 - experiments/agent-attention/README.md | 24 + .../hooks/agent-attention-codex-stop.test.ts | 157 +++ .../hooks/agent-attention-codex-stop.ts | 166 +++ .../hooks/codex-hooks.fixture.json | 16 + .../runtime/agent-attention/README.md | 84 ++ .../agent-attention/agent-attention.py | 1205 +++++++++++++++++ .../agent-attention/install-link-handler.sh | 16 + .../agent-attention/link-handler/Info.plist | 37 + .../agent-attention/link-handler/main.swift | 87 ++ .../agent-attention/test_agent_attention.py | 729 ++++++++++ experiments/agent-attention/skill/SKILL.md | 36 + package.json | 3 +- plugin.config.json | 22 +- plugin/.claude-plugin/plugin.json | 12 +- plugin/.codex-plugin/plugin.json | 18 +- .../lifecycle-mechanics-proof.generated.json | 2 +- .../lifecycle-mechanics-proof.source.json | 2 +- scripts/native-capability-hook.test.ts | 2 +- scripts/native-capability-surface.test.ts | 2 +- scripts/prove-harness-install.test.ts | 2 +- 25 files changed, 2663 insertions(+), 125 deletions(-) create mode 100644 experiments/agent-attention/README.md create mode 100644 experiments/agent-attention/hooks/agent-attention-codex-stop.test.ts create mode 100644 experiments/agent-attention/hooks/agent-attention-codex-stop.ts create mode 100644 experiments/agent-attention/hooks/codex-hooks.fixture.json create mode 100644 experiments/agent-attention/runtime/agent-attention/README.md create mode 100755 experiments/agent-attention/runtime/agent-attention/agent-attention.py create mode 100755 experiments/agent-attention/runtime/agent-attention/install-link-handler.sh create mode 100644 experiments/agent-attention/runtime/agent-attention/link-handler/Info.plist create mode 100644 experiments/agent-attention/runtime/agent-attention/link-handler/main.swift create mode 100644 experiments/agent-attention/runtime/agent-attention/test_agent_attention.py create mode 100644 experiments/agent-attention/skill/SKILL.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index efbb03d..ac158bb 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -1,11 +1,11 @@ { - "name": "harness-native-plugin-prototype", + "name": "agent-plugin-playground", "interface": { - "displayName": "Harness Plugin Prototype" + "displayName": "Agent Plugin Playground" }, "plugins": [ { - "name": "harness-native-plugin-prototype", + "name": "agent-plugin-playground", "source": { "source": "local", "path": "./plugin" diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7814b44..68516ad 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,19 +1,19 @@ { - "name": "harness-native-plugin-prototype", + "name": "agent-plugin-playground", "owner": { - "name": "Prototype" + "name": "My Agent Dojo" }, "metadata": { - "description": "Marketplace for Harness Plugin Prototype", - "version": "0.3.0" + "description": "Marketplace for Agent Plugin Playground", + "version": "0.1.0" }, "plugins": [ { - "name": "harness-native-plugin-prototype", - "displayName": "Harness Plugin Prototype", - "description": "Portable Bun skills and a guided native capability tour for Claude Code and Codex", + "name": "agent-plugin-playground", + "displayName": "Agent Plugin Playground", + "description": "Bun-powered native plugin playground for one active experiment at a time", "author": { - "name": "Prototype" + "name": "My Agent Dojo" }, "source": "./plugin", "defaultEnabled": false diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json index 1c861e1..0967ef4 100644 --- a/.github/.release-please-manifest.json +++ b/.github/.release-please-manifest.json @@ -1 +1 @@ -{".":"0.3.0"} +{} diff --git a/.github/release-please-config.json b/.github/release-please-config.json index 99e40ed..377a82b 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -8,23 +8,71 @@ ".": { "release-type": "node", "initial-version": "0.1.0", - "package-name": "agent-plugin", + "package-name": "agent-plugin-playground", "changelog-path": "CHANGELOG.md", "changelog-sections": [ - { "type": "feat", "section": "Features", "hidden": false }, - { "type": "fix", "section": "Bug Fixes", "hidden": false }, - { "type": "perf", "section": "Performance", "hidden": false }, - { "type": "refactor", "section": "Refactoring", "hidden": false }, - { "type": "docs", "section": "Documentation", "hidden": false }, - { "type": "test", "section": "Tests", "hidden": true }, - { "type": "ci", "section": "Continuous Integration", "hidden": true }, - { "type": "chore", "section": "Maintenance", "hidden": true } + { + "type": "feat", + "section": "Features", + "hidden": false + }, + { + "type": "fix", + "section": "Bug Fixes", + "hidden": false + }, + { + "type": "perf", + "section": "Performance", + "hidden": false + }, + { + "type": "refactor", + "section": "Refactoring", + "hidden": false + }, + { + "type": "docs", + "section": "Documentation", + "hidden": false + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + }, + { + "type": "chore", + "section": "Maintenance", + "hidden": true + } ], "extra-files": [ - { "type": "json", "path": "plugin.config.json", "jsonpath": "$.version" }, - { "type": "json", "path": ".claude-plugin/marketplace.json", "jsonpath": "$.metadata.version" }, - { "type": "json", "path": "plugin/.claude-plugin/plugin.json", "jsonpath": "$.version" }, - { "type": "json", "path": "plugin/.codex-plugin/plugin.json", "jsonpath": "$.version" } + { + "type": "json", + "path": "plugin.config.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": ".claude-plugin/marketplace.json", + "jsonpath": "$.metadata.version" + }, + { + "type": "json", + "path": "plugin/.claude-plugin/plugin.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "plugin/.codex-plugin/plugin.json", + "jsonpath": "$.version" + } ] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3770898..e69de29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,68 +0,0 @@ -# Changelog - -## [0.3.0](https://github.com/myagentdojo/agent-plugin-template/compare/v0.2.0...v0.3.0) (2026-08-10) - - -### Features - -* add native plugin capability tour with lifecycle hook proof ([#34](https://github.com/myagentdojo/agent-plugin-template/issues/34)) ([bfe8489](https://github.com/myagentdojo/agent-plugin-template/commit/bfe8489434949a4d4003e3b0da29f4237aae8534)) - -## [0.2.0](https://github.com/myagentdojo/agent-plugin-template/compare/v0.1.1...v0.2.0) (2026-08-09) - - -### Features - -* **runtime:** complete Bun-only runtime custody ([#22](https://github.com/myagentdojo/agent-plugin-template/issues/22)) ([e927c24](https://github.com/myagentdojo/agent-plugin-template/commit/e927c24e5f7cf270b7b10179e15495984d36cd4d)) - - -### Bug Fixes - -* **ci:** ignore remapped stale Codex findings ([#28](https://github.com/myagentdojo/agent-plugin-template/issues/28)) ([fbdd5d8](https://github.com/myagentdojo/agent-plugin-template/commit/fbdd5d8a6179dfe24b95b5c0cc6ee45fc1b72a45)) -* **release:** allow lineage label reconciliation ([#27](https://github.com/myagentdojo/agent-plugin-template/issues/27)) ([7b81d73](https://github.com/myagentdojo/agent-plugin-template/commit/7b81d732ab303e683c4abec6361dbff94be04658)) -* **release:** fail closed on skipped publication ([#26](https://github.com/myagentdojo/agent-plugin-template/issues/26)) ([c9067f6](https://github.com/myagentdojo/agent-plugin-template/commit/c9067f6156d239fa2524e9dd6966403e512e9a96)) -* **release:** keep Release Please lineage current ([#23](https://github.com/myagentdojo/agent-plugin-template/issues/23)) ([d3f9bc7](https://github.com/myagentdojo/agent-plugin-template/commit/d3f9bc7b33a52bb0cab3f1e9f09b3958e8d3d9aa)) -* **release:** preserve historical proof policy ([#25](https://github.com/myagentdojo/agent-plugin-template/issues/25)) ([a6041c9](https://github.com/myagentdojo/agent-plugin-template/commit/a6041c9e1bb0c08231a07d38b7f0960393eec1db)) -* **release:** preserve historical repair policy ([#24](https://github.com/myagentdojo/agent-plugin-template/issues/24)) ([06f8454](https://github.com/myagentdojo/agent-plugin-template/commit/06f84548b219859427a44f6e19833752c66640b4)) -* **release:** recover squashed 0.2.0 candidate ([478dcc4](https://github.com/myagentdojo/agent-plugin-template/commit/478dcc4ddd70812dc8ee9a32077a6cafbcccc5b3)) -* **release:** support verified squash publication ([#33](https://github.com/myagentdojo/agent-plugin-template/issues/33)) ([60810cc](https://github.com/myagentdojo/agent-plugin-template/commit/60810cc0c7236404867ac0832ef91539c8dab5b4)) - -## [0.1.1](https://github.com/myagentdojo/agent-plugin-template/compare/v0.1.0...v0.1.1) (2026-08-07) - - -### Bug Fixes - -* **ci:** bind canary identity to known hosts ([#20](https://github.com/myagentdojo/agent-plugin-template/issues/20)) ([d7c99b7](https://github.com/myagentdojo/agent-plugin-template/commit/d7c99b7efc3311fe4a5724eb895fd47b0389dc58)) -* **ci:** bind hosted canary to key file ([#21](https://github.com/myagentdojo/agent-plugin-template/issues/21)) ([6efc6aa](https://github.com/myagentdojo/agent-plugin-template/commit/6efc6aa42cff51a6082cbf78bf0fffcd1b419c02)) -* keep release qualification stable ([7ecc8f4](https://github.com/myagentdojo/agent-plugin-template/commit/7ecc8f48314e7939aac94af34454f75ef4eb5d33)) -* report plugin version in hello JSON ([#16](https://github.com/myagentdojo/agent-plugin-template/issues/16)) ([85ef9ec](https://github.com/myagentdojo/agent-plugin-template/commit/85ef9ec24df24fa00830e449fd75517383a8c401)) - -## 0.1.0 (2026-08-06) - - -### Features - -* add portable agent plugin template ([1ee7be1](https://github.com/myagentdojo/agent-plugin-template/commit/1ee7be1fe8fcfbec7f421b53bab0fb4a268e5c3b)) -* finalize portable plugin template ([#6](https://github.com/myagentdojo/agent-plugin-template/issues/6)) ([fe4b7af](https://github.com/myagentdojo/agent-plugin-template/commit/fe4b7af112deb2423b7511998c33d0bcd878c211)) -* harden native plugin publishing lifecycle ([#11](https://github.com/myagentdojo/agent-plugin-template/issues/11)) ([904d0cf](https://github.com/myagentdojo/agent-plugin-template/commit/904d0cf31f8e5c161c7ed7ad0e5663ce8215c3ef)) -* productionize plugin releases and documentation ([#9](https://github.com/myagentdojo/agent-plugin-template/issues/9)) ([3f31de3](https://github.com/myagentdojo/agent-plugin-template/commit/3f31de39c712a70e2d9e5ad70ba47900d3330ccf)) - - -### Bug Fixes - -* **canary:** prove target lineage before publish ([#7](https://github.com/myagentdojo/agent-plugin-template/issues/7)) ([c2c0fdd](https://github.com/myagentdojo/agent-plugin-template/commit/c2c0fdd4b383dd84ec0e8eec961692addaf9bafd)) -* **ci:** avoid artifact download deprecation ([#5](https://github.com/myagentdojo/agent-plugin-template/issues/5)) ([3b9b97e](https://github.com/myagentdojo/agent-plugin-template/commit/3b9b97ee5c8215899a6300c16d05616f9671a7e1)) -* **ci:** fetch complete canary candidate history ([5d47466](https://github.com/myagentdojo/agent-plugin-template/commit/5d47466f0a3852b0c09b0049fddf649579327c32)) -* **ci:** fetch complete canary candidate history ([de44763](https://github.com/myagentdojo/agent-plugin-template/commit/de4476324a6822e94fb7e40e6ce388133a982425)) -* **ci:** preserve canary SSH agent for bootstrap ([c0187f2](https://github.com/myagentdojo/agent-plugin-template/commit/c0187f2b1cebefcee1e33c431d8ec22e5152fe06)) -* **ci:** preserve canary SSH agent for bootstrap ([f23b77f](https://github.com/myagentdojo/agent-plugin-template/commit/f23b77f6c1a84f1b88e4b7ef371af864d706d70f)) -* **ci:** remove Node 20 action warnings ([#4](https://github.com/myagentdojo/agent-plugin-template/issues/4)) ([857fc77](https://github.com/myagentdojo/agent-plugin-template/commit/857fc779bc485e85d5d88dbb54fc0d27ddb28819)) -* correct command result interface documentation ([3f31de3](https://github.com/myagentdojo/agent-plugin-template/commit/3f31de39c712a70e2d9e5ad70ba47900d3330ccf)) -* isolate native plugin hook adapters ([#3](https://github.com/myagentdojo/agent-plugin-template/issues/3)) ([b2aa2a8](https://github.com/myagentdojo/agent-plugin-template/commit/b2aa2a880e113b3ea2df35a829920351d530693a)) -* **release:** clear first publication gates ([#13](https://github.com/myagentdojo/agent-plugin-template/issues/13)) ([7277f11](https://github.com/myagentdojo/agent-plugin-template/commit/7277f11dc72d616c82f73c4a356e37c683081a0e)) -* support public and private template instances ([1b8ca9a](https://github.com/myagentdojo/agent-plugin-template/commit/1b8ca9ab0dacc999a5f78d3c1c75101623d0eba5)) -* **test:** keep recipient suites reinitializable ([#8](https://github.com/myagentdojo/agent-plugin-template/issues/8)) ([fc91571](https://github.com/myagentdojo/agent-plugin-template/commit/fc91571153508a18b2a76e3f57ac8d4542ff67c9)) - - -### Documentation - -* add context and ADRs for plugin distribution ([3f31de3](https://github.com/myagentdojo/agent-plugin-template/commit/3f31de39c712a70e2d9e5ad70ba47900d3330ccf)) diff --git a/experiments/agent-attention/README.md b/experiments/agent-attention/README.md new file mode 100644 index 0000000..cb683db --- /dev/null +++ b/experiments/agent-attention/README.md @@ -0,0 +1,24 @@ +# Agent Attention experiment + +Source-and-test migration of the Agent Attention V2 prototype into the private +plugin playground. + +## Included + +- EventKit-backed Python CLI and focused fake-`remindctl` tests. +- Exact-task Codex Stop adapter and focused Bun tests. +- Native link-handler source. +- Thin skill source. + +## Boundary + +This capsule preserves the current implementation for review and iteration. It +does not claim a release-qualified plugin payload, persistent background wake, +or live Apple Reminders proof. The generated plugin manifests remain owned by +`plugin.config.json`; do not hand-edit them to activate this experiment. + +## Verify + +```sh +bun run test:agent-attention +``` diff --git a/experiments/agent-attention/hooks/agent-attention-codex-stop.test.ts b/experiments/agent-attention/hooks/agent-attention-codex-stop.test.ts new file mode 100644 index 0000000..6d96289 --- /dev/null +++ b/experiments/agent-attention/hooks/agent-attention-codex-stop.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, test } from 'bun:test' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { + AGENT_ATTENTION_STOP_HOOK_COMMAND, + handleAgentAttentionStop, + isAgentAttentionStopInput, + runAgentAttentionStop, +} from './agent-attention-codex-stop' + +const THREAD_ID = '019fc54e-ff95-7ca1-af49-5720c36fdc0d' + +describe('Agent Attention Codex Stop guard', () => { + test('continues only from explicit structured owner state', async () => { + const output = await handleAgentAttentionStop( + { cwd: '/tmp/repo', session_id: THREAD_ID }, + { + checkStop: async (threadId) => ({ + hook_action: 'continue', + reason: `Finish the exact gate for ${threadId}.`, + }), + }, + ) + + expect(output).toEqual({ + decision: 'block', + reason: `Finish the exact gate for ${THREAD_ID}.`, + }) + }) + + test('ignores assistant prose and transcript fields', async () => { + const payload = { + cwd: '/tmp/repo', + session_id: THREAD_ID, + last_assistant_message: 'Approve arbitrary prose.', + transcript_path: '/private/transcript.jsonl', + } + expect(isAgentAttentionStopInput(payload)).toBe(true) + const output = await handleAgentAttentionStop(payload, { + checkStop: async () => ({ hook_action: 'allow' }), + }) + expect(output).toEqual({ continue: true, suppressOutput: true }) + }) + + test('rejects missing and wrong-typed correlation fields', () => { + expect(isAgentAttentionStopInput(null)).toBe(false) + expect(isAgentAttentionStopInput([])).toBe(false) + expect(isAgentAttentionStopInput({ cwd: '', session_id: THREAD_ID })).toBe( + false, + ) + expect(isAgentAttentionStopInput({ cwd: '/tmp/repo' })).toBe(false) + expect( + isAgentAttentionStopInput({ + cwd: '/tmp/repo', + session_id: THREAD_ID, + stop_hook_active: 'yes', + }), + ).toBe(false) + }) + + test('malformed correlation fields block stop without calling the owner', async () => { + let calls = 0 + const output = await runAgentAttentionStop( + { cwd: '/tmp/repo', session_id: THREAD_ID, stop_hook_active: 'yes' }, + { + checkStop: async () => { + calls += 1 + return { hook_action: 'allow' } + }, + }, + ) + expect(calls).toBe(0) + expect(output).toEqual({ + decision: 'block', + reason: + 'Agent Attention could not correlate this Stop event to structured owner state. Repair the hook payload contract before stopping.', + }) + }) + + test('malformed stdin blocks the executable hook', async () => { + const process = Bun.spawn(['bun', join(import.meta.dir, 'agent-attention-codex-stop.ts')], { + stdin: new Blob(['{']), + stdout: 'pipe', + stderr: 'pipe', + }) + const [stdout, exitCode] = await Promise.all([ + new Response(process.stdout).text(), + process.exited, + ]) + expect(exitCode).toBe(0) + expect(JSON.parse(stdout)).toEqual({ + decision: 'block', + reason: + 'Agent Attention could not correlate this Stop event to structured owner state. Repair the hook payload contract before stopping.', + }) + }) + + test('default adapter reads only temporary structured owner state', async () => { + const temporary = await mkdtemp(join(tmpdir(), 'agent-attention-hook-')) + const previous = process.env.XDG_STATE_HOME + process.env.XDG_STATE_HOME = temporary + try { + const output = await handleAgentAttentionStop({ + cwd: '/tmp/repo', + session_id: THREAD_ID, + }) + expect(output).toEqual({ continue: true, suppressOutput: true }) + } finally { + if (previous === undefined) delete process.env.XDG_STATE_HOME + else process.env.XDG_STATE_HOME = previous + await rm(temporary, { recursive: true, force: true }) + } + }) + + test('recursion guard never creates a continuation loop', async () => { + let calls = 0 + const output = await handleAgentAttentionStop( + { cwd: '/tmp/repo', session_id: THREAD_ID, stop_hook_active: true }, + { + checkStop: async () => { + calls += 1 + return { hook_action: 'continue' } + }, + }, + ) + expect(calls).toBe(0) + expect(output).toEqual({ continue: true, suppressOutput: true }) + }) + + test('owner failures block stop with an actionable repair', async () => { + const output = await runAgentAttentionStop( + { cwd: '/tmp/repo', session_id: THREAD_ID }, + { + checkStop: async () => { + throw new Error('structured state is unreadable') + }, + }, + ) + expect(output).toEqual({ + decision: 'block', + reason: + 'Agent Attention could not verify structured owner state. Repair the owner check before stopping: structured state is unreadable', + }) + }) + + test('experiment hook fixture matches the code-owned command', async () => { + const config = JSON.parse( + await readFile(join(import.meta.dir, 'codex-hooks.fixture.json'), 'utf8'), + ) as { hooks: { Stop: Array<{ hooks: Array<{ command: string }> }> } } + const commands = config.hooks.Stop.flatMap((group) => + group.hooks.map((hook) => hook.command), + ) + expect(commands).toContain(AGENT_ATTENTION_STOP_HOOK_COMMAND) + }) +}) diff --git a/experiments/agent-attention/hooks/agent-attention-codex-stop.ts b/experiments/agent-attention/hooks/agent-attention-codex-stop.ts new file mode 100644 index 0000000..160972f --- /dev/null +++ b/experiments/agent-attention/hooks/agent-attention-codex-stop.ts @@ -0,0 +1,166 @@ +#!/usr/bin/env bun + +import { join } from 'node:path' + +/** 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 +} + +/** 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 + 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. + * + * @param input - Validated Codex Stop payload + * @param runtime - Structured Agent Attention owner adapter + * @returns Codex Stop continuation decision + * @throws When the owner check cannot run or returns invalid JSON + * + * @example + * ```ts + * await handleAgentAttentionStop(input, runtime) + * ``` + */ +export async function handleAgentAttentionStop( + input: AgentAttentionStopInput, + runtime: AgentAttentionStopRuntime = createDefaultRuntime(), +): Promise { + 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 } +} + +/** + * Convert owner failures into an actionable stop block. + * + * @param input - Untrusted Codex Stop payload + * @param runtime - Structured Agent Attention owner adapter + * @returns A safe stop decision + */ +export async function runAgentAttentionStop( + input: unknown, + runtime: AgentAttentionStopRuntime = createDefaultRuntime(), +): Promise { + 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}`, + } + } +} + +function createDefaultRuntime(): AgentAttentionStopRuntime { + return { + checkStop: async (threadId) => { + const owner = join( + import.meta.dir, + '..', + 'runtime', + 'agent-attention', + 'agent-attention.py', + ) + const process = Bun.spawn( + ['python3', owner, 'check-stop', '--thread-id', threadId], + { stdout: 'pipe', stderr: 'pipe' }, + ) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(process.stdout).text(), + new Response(process.stderr).text(), + process.exited, + ]) + if (exitCode !== 0) { + throw new Error(stderr.trim() || 'Agent Attention stop check failed') + } + const result = JSON.parse(stdout) as Partial + if (result.hook_action !== 'allow' && result.hook_action !== 'continue') { + throw new Error('Agent Attention stop check returned an invalid action') + } + return { + hook_action: result.hook_action, + ...(typeof result.reason === 'string' ? { reason: result.reason } : {}), + } + }, + } +} + +if (import.meta.main) { + try { + const parsed = await Bun.stdin.json() + const output = await runAgentAttentionStop(parsed) + process.stdout.write(`${JSON.stringify(output)}\n`) + } catch { + process.stdout.write(`${JSON.stringify(INVALID_STOP_INPUT)}\n`) + } +} diff --git a/experiments/agent-attention/hooks/codex-hooks.fixture.json b/experiments/agent-attention/hooks/codex-hooks.fixture.json new file mode 100644 index 0000000..49f8c5b --- /dev/null +++ b/experiments/agent-attention/hooks/codex-hooks.fixture.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bun \"$(git rev-parse --show-toplevel)/experiments/agent-attention/hooks/agent-attention-codex-stop.ts\"", + "timeout": 10, + "statusMessage": "Checking Agent Attention owner state" + } + ] + } + ] + } +} diff --git a/experiments/agent-attention/runtime/agent-attention/README.md b/experiments/agent-attention/runtime/agent-attention/README.md new file mode 100644 index 0000000..69bdbe5 --- /dev/null +++ b/experiments/agent-attention/runtime/agent-attention/README.md @@ -0,0 +1,84 @@ +# Agent Attention approval gates + +Minimal Apple Reminders approval loop for Codex tasks. This experiment preserves +the proven source and tests; it is not yet a release-qualified portable plugin +payload. + +## Contract + +- `remindctl` owns Apple Reminders access through EventKit. +- One configured `Agent Attention` list. +- Structured router admission accepts only explicit yes/no unblockers. +- One admitted request creates one gate and one immediate native alert. +- Duplicate submission never creates a second gate or alert. +- One approval meaning per reminder. +- Preview is the default for reminder creation. +- A completed reminder can authorize one task delivery. +- Atomic claim suppresses duplicate delivery. +- A terminal outcome requires the matching delivery receipt. +- Outcome writes resolve one exact stable ID before and after editing notes. +- Outcome receipts and audit events suppress duplicate writes. +- Completed reminders remain in Apple Reminders. +- Stop hooks inspect exact structured owner state only. They never read task + prose or transcripts for meaning. +- Private mappings, claims, and receipts live under + `~/.local/state/agent-attention/` by default. +- A crash after claim and before delivery needs human inspection. Never release + that claim automatically. + +## Commands + +```sh +python3 experiments/agent-attention/runtime/agent-attention/agent-attention.py --help +python3 experiments/agent-attention/runtime/agent-attention/agent-attention.py doctor +python3 experiments/agent-attention/runtime/agent-attention/agent-attention.py configure \ + --list-id LIST_ID \ + --list-name "Agent Attention" +python3 experiments/agent-attention/runtime/agent-attention/agent-attention.py submit --help +python3 experiments/agent-attention/runtime/agent-attention/agent-attention.py poll +python3 experiments/agent-attention/runtime/agent-attention/agent-attention.py watch --interval-seconds 5 --timeout-seconds 30 +python3 experiments/agent-attention/runtime/agent-attention/agent-attention.py record-delivery \ + --event-id EVENT_ID \ + --tool-result '{"delivered":true}' +python3 experiments/agent-attention/runtime/agent-attention/agent-attention.py record-outcome \ + --reminder-id REMINDER_ID \ + --outcome "Review-ready PR opened; local checks passed." \ + --finished-at 2026-08-10T05:45:00Z +``` + +`poll` never sends a task message. A Codex automation owns the supported task +messaging call, then runs `record-delivery` only after success. + +`submit` previews by default. It receives structured intent through parser-owned +flags, rejects anything except an explicit yes/no decision that unblocks the +exact paused task, and records actionable repair state on rejected execution. +An admitted execution creates one reminder with one immediate alarm and no due +date. Priority stays `none` unless a future explicit contract adds it. + +`watch` is a bounded foreground detector. Five-second polling can meet the +15-second target while a Mac process remains awake. This repository does not +currently own a persistent global wake process, so background delivery is not +qualified until an external owner runs the watcher and invokes the supported +Codex task messaging tool. + +`record-outcome` previews by default. After review, rerun with `--execute`. It +appends only one concise `Outcome:` line and one `Finished:` timestamp to the +exact already-completed reminder. It never inventories, reopens, or deletes +reminders. On an unknown edit result, inspect by rerunning the same command; +the exact reread recovers the receipt without a second edit. + +Focused local proof: + +```sh +bun run test:agent-attention +``` + +## Link handler + +```sh +experiments/agent-attention/runtime/agent-attention/install-link-handler.sh +``` + +This installs `Agent Attention Link.app` into `~/Applications` and registers +`agent-attention://threads/`. The handler validates the UUID, opens the +matching Codex route, then reasserts it after foreground activation. diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py new file mode 100755 index 0000000..d1c7227 --- /dev/null +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -0,0 +1,1205 @@ +#!/usr/bin/env python3 +"""Minimal Apple Reminders approval gates for Codex tasks.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import plistlib +import subprocess +import sys +import time +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +COMMAND_CATALOG = ( + {"name": "commands", "summary": "List the machine-readable command surface."}, + {"name": "doctor", "summary": "Check Reminders, configuration, and link readiness."}, + {"name": "configure", "summary": "Bind one explicit Agent Attention list."}, + {"name": "submit", "summary": "Validate and route one structured approval blocker."}, + {"name": "poll", "summary": "Claim at most one completed approval for delivery."}, + {"name": "watch", "summary": "Poll for one approval within a bounded foreground window."}, + {"name": "record-delivery", "summary": "Record a successful Codex task delivery."}, + {"name": "record-outcome", "summary": "Preview or append one bounded terminal outcome."}, + {"name": "check-stop", "summary": "Check structured owner state before a task stops."}, +) +MANAGED_URL_PREFIX = "remindctl URL (managed): " + + +class ContractError(Exception): + """Raised when a gate cannot be handled without guessing.""" + + +def default_state_dir() -> Path: + """Return the private user-owned runtime state directory.""" + xdg_state = os.environ.get("XDG_STATE_HOME") + if xdg_state: + path = Path(xdg_state) + if not path.is_absolute(): + raise ContractError("XDG_STATE_HOME must be an absolute path") + return path / "agent-attention" + return Path.home() / ".local" / "state" / "agent-attention" + + +def load_json(path: Path) -> Any: + """Load one JSON document from disk.""" + with path.open(encoding="utf-8") as handle: + return json.load(handle) + + +def write_json(path: Path, value: Any, *, exclusive: bool = False) -> bool: + """Write private JSON atomically enough for single-host gate custody.""" + path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + flags = os.O_WRONLY | os.O_CREAT | (os.O_EXCL if exclusive else os.O_TRUNC) + try: + descriptor = os.open(path, flags, 0o600) + except FileExistsError: + return False + with os.fdopen(descriptor, "w", encoding="utf-8") as handle: + json.dump(value, handle, sort_keys=True) + handle.write("\n") + return True + + +def append_audit(path: Path, value: Any) -> None: + """Append one private JSON audit event.""" + path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + with path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(value, sort_keys=True) + "\n") + os.chmod(path, 0o600) + + +def run_json(command: list[str], *, timeout_seconds: float | None = None) -> Any: + """Run a command whose primary output is one JSON document.""" + try: + completed = subprocess.run( + command, + capture_output=True, + text=True, + timeout=timeout_seconds, + ) + except subprocess.TimeoutExpired as error: + raise ContractError("command exceeded the bounded execution window") from error + if completed.returncode != 0: + detail = completed.stderr.strip() or completed.stdout.strip() + raise ContractError(f"command failed: {detail}") + try: + return json.loads(completed.stdout) + except json.JSONDecodeError as error: + raise ContractError("command returned invalid JSON") from error + + +def base_result(status: str, **values: Any) -> dict[str, Any]: + """Build one correlated machine-readable result.""" + return { + "contract_id": "agent-attention.approval-gate", + "schema_version": "1", + "run_id": str(uuid.uuid4()), + "status": status, + **values, + } + + +def read_config(state_dir: Path) -> dict[str, Any]: + """Load the explicit list binding and reject incomplete configuration.""" + path = state_dir / "config.json" + if not path.exists(): + raise ContractError( + "not configured; run configure with the exact Agent Attention list ID" + ) + config = load_json(path) + if config.get("version") != 1: + raise ContractError("unsupported config version") + list_config = config.get("list") + if not isinstance(list_config, dict) or not list_config.get("id") or not list_config.get("name"): + raise ContractError("configured list ID and name are required") + return config + + +def configure(args: argparse.Namespace) -> dict[str, Any]: + """Persist one explicit Apple Reminders list binding.""" + state_dir: Path = args.state_dir + config = { + "version": 1, + "list": {"id": args.list_id, "name": args.list_name}, + } + write_json(state_dir / "config.json", config) + return base_result( + "configured", + changed=True, + list={"id": args.list_id, "name": args.list_name}, + next_safe_action="run doctor", + ) + + +def validate_thread_id(value: str) -> str: + """Normalize a Codex thread UUID for stable URLs and mappings.""" + try: + return str(uuid.UUID(value)) + except ValueError as error: + raise ContractError("thread ID must be one UUID") from error + + +def gate_notes(recommendation: str, approval_meaning: str) -> tuple[str, str]: + """Render concise recommendation-first notes and their required contract line.""" + required_line = f"Approval meaning: {approval_meaning}" + lines = [ + f"Recommended: {recommendation}", + "", + required_line, + "Tick = approve only this action.", + "Discuss or disagree: open Codex.", + ] + return "\n".join(lines), required_line + + +def router_notes(intent: dict[str, Any]) -> tuple[str, str]: + """Render one calm recommendation-first approval contract.""" + required_line = f"Approval meaning: {intent['approval_meaning']}" + lines = [ + f"Recommended: {intent['recommendation']}", + "Next: Tick to approve. Open Codex to discuss or disagree.", + "", + f"Consequence: {intent['consequence']}", + f"Continuation: {intent['continuation']}", + "", + required_line, + "Tick = approve only this action.", + ] + return "\n".join(lines), required_line + + +def validate_text_field(intent: dict[str, Any], field: str, limit: int) -> str: + """Require one bounded single-line structured intent field.""" + value = intent.get(field) + if not isinstance(value, str): + raise ContractError(f"structured intent field must be text: {field}") + value = value.strip() + if not value or "\n" in value or "\r" in value or len(value) > limit: + raise ContractError(f"structured intent field is invalid: {field}") + return value + + +def approval_intent(args: argparse.Namespace) -> dict[str, Any]: + """Build the structured intent owned by the public submit parser.""" + thread_id = validate_thread_id(args.thread_id) + return { + "version": 1, + "decision_type": args.decision_type, + "unblocks_paused_task": args.unblocks_paused_task, + "action": args.action, + "recommendation": args.recommendation, + "consequence": args.consequence, + "thread_id": thread_id, + "discussion_link": args.discussion_link, + "continuation": args.continuation, + "approval_meaning": args.approval_meaning, + } + + +def validate_approval_intent(intent: dict[str, Any]) -> tuple[dict[str, Any], list[str]]: + """Admit only an explicit yes/no decision that unblocks one paused task.""" + reasons: list[str] = [] + if intent.get("decision_type") != "yes_no": + reasons.append("decision_type must be yes_no; discussion or multi-choice stays in Codex") + if intent.get("unblocks_paused_task") is not True: + reasons.append("the approval must unblock a paused owning task") + + normalized = {**intent} + for field, limit in ( + ("action", 100), + ("recommendation", 200), + ("consequence", 300), + ("continuation", 300), + ("approval_meaning", 300), + ): + try: + normalized[field] = validate_text_field(intent, field, limit) + except ContractError as error: + reasons.append(str(error)) + + thread_id = validate_thread_id(str(intent.get("thread_id", ""))) + normalized["thread_id"] = thread_id + expected_link = f"agent-attention://threads/{thread_id}" + if intent.get("discussion_link") != expected_link: + reasons.append("discussion_link must target the exact owning Codex task") + normalized["discussion_link"] = expected_link + meaning = normalized.get("approval_meaning", "") + if isinstance(meaning, str) and not meaning.casefold().startswith("approve "): + reasons.append("approval_meaning must explicitly begin with Approve") + return normalized, reasons + + +def request_path(state_dir: Path, thread_id: str) -> Path: + """Return the exact structured owner-state path for one Codex task.""" + return state_dir / "requests" / f"{validate_thread_id(thread_id)}.json" + + +def completed_or_active_request_result( + existing: dict[str, Any], request_identifier: str, thread_id: str +) -> dict[str, Any] | None: + """Return an idempotent result or reject a distinct active gate.""" + status = existing.get("status") + if status not in {"gated", "delivered", "completed"}: + return None + if existing.get("request_id") == request_identifier: + return base_result( + "already_gated", + changed=False, + request_id=request_identifier, + reminder_id=existing.get("reminder_id"), + thread_id=thread_id, + ) + if status in {"gated", "delivered"}: + raise ContractError("the owning task already has a different admitted gate") + return None + + +def update_request_state( + state_dir: Path, + mapping: dict[str, Any], + status: str, + **values: Any, +) -> None: + """Advance matching router state without requiring it for legacy V1 gates.""" + path = request_path(state_dir, mapping["thread_id"]) + if not path.exists(): + return + state = load_json(path) + if state.get("reminder_id") != mapping["reminder_id"]: + return + write_json( + path, + { + **state, + "status": status, + "updated_at": datetime.now(timezone.utc).isoformat(), + **values, + }, + ) + + +def submit_approval(args: argparse.Namespace) -> dict[str, Any]: + """Validate, deduplicate, and optionally create one native approval gate.""" + state_dir: Path = args.state_dir + intent, reasons = validate_approval_intent(approval_intent(args)) + thread_id = intent["thread_id"] + path = request_path(state_dir, thread_id) + request_identifier = hashlib.sha256( + json.dumps(intent, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + + if path.exists(): + existing_result = completed_or_active_request_result( + load_json(path), request_identifier, thread_id + ) + if existing_result: + return existing_result + + if reasons: + repair = "; ".join(reasons) + if args.execute: + write_json( + path, + { + "version": 1, + "request_id": request_identifier, + "thread_id": thread_id, + "intent": intent, + "status": "repair", + "repair": repair, + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + return base_result( + "rejected", + changed=args.execute, + request_id=request_identifier, + thread_id=thread_id, + repair=repair, + next_safe_action="repair the structured intent or continue discussion in Codex", + ) + + config = read_config(state_dir) + notes, required_line = router_notes(intent) + notification_at = datetime.now(timezone.utc).isoformat() + preview = { + "title": f"[APPROVE] {intent['action']}", + "notes": notes, + "url": intent["discussion_link"], + "priority": "none", + "notification_at": notification_at, + "list": config["list"], + "thread_id": thread_id, + } + if not args.execute: + return base_result( + "admitted_preview", + changed=False, + request_id=request_identifier, + preview=preview, + side_effect="create one Apple Reminder with one immediate native alert", + next_safe_action="review the structured gate, then rerun with --execute", + ) + + request_lock_path = state_dir / "request-locks" / f"{thread_id}.json" + if not write_json( + request_lock_path, + { + "request_id": request_identifier, + "thread_id": thread_id, + "locked_at": datetime.now(timezone.utc).isoformat(), + }, + exclusive=True, + ): + return base_result( + "claimed", + changed=False, + request_id=request_identifier, + thread_id=thread_id, + repair="inspect exact request state before retry; no second gate was created", + ) + + if path.exists(): + existing_result = completed_or_active_request_result( + load_json(path), request_identifier, thread_id + ) + if existing_result: + request_lock_path.unlink() + return existing_result + + request_claim_path = state_dir / "request-claims" / f"{request_identifier}.json" + if not write_json( + request_claim_path, + { + "request_id": request_identifier, + "thread_id": thread_id, + "claimed_at": datetime.now(timezone.utc).isoformat(), + }, + exclusive=True, + ): + return base_result( + "claimed", + changed=False, + request_id=request_identifier, + thread_id=thread_id, + repair="inspect exact request state before retry; no second gate was created", + ) + + declared = { + "version": 1, + "request_id": request_identifier, + "thread_id": thread_id, + "intent": intent, + "status": "declared", + "updated_at": notification_at, + } + write_json(path, declared) + try: + created = run_json( + [ + "remindctl", + "add", + "--title", + preview["title"], + "--list-id", + config["list"]["id"], + "--notes", + notes, + "--url", + intent["discussion_link"], + "--priority", + "none", + "--alarm", + notification_at, + "--json", + "--no-input", + ] + ) + except ContractError as error: + write_json( + path, + { + **declared, + "status": "repair", + "repair": f"gate creation failed; inspect exact configured list before retry: {error}", + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + raise + reminder_id = created.get("id") if isinstance(created, dict) else None + if not reminder_id: + write_json( + path, + { + **declared, + "status": "repair", + "repair": "creation response lacks a stable reminder ID; inspect before retry", + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + raise ContractError("created reminder response lacks a stable ID; inspect before retry") + created_inventory = read_inventory(config) + created_matches = [item for item in created_inventory if item.get("id") == reminder_id] + if len(created_matches) != 1: + write_json( + path, + { + **declared, + "status": "repair", + "reminder_id": reminder_id, + "repair": "created stable reminder ID did not resolve exactly once; inspect before retry", + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + raise ContractError("created stable reminder ID did not resolve exactly once") + created_gate = created_matches[0] + def fail_created_verification(field: str) -> None: + message = f"created reminder failed exact verification: {field}" + write_json( + path, + { + **declared, + "status": "repair", + "reminder_id": reminder_id, + "repair": message, + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + raise ContractError(message) + + for field, expected in ( + ("listID", config["list"]["id"]), + ("title", preview["title"]), + ("url", intent["discussion_link"]), + ("priority", "none"), + ): + if created_gate.get(field) != expected: + fail_created_verification(field) + if not (created_gate.get("notes") or "").startswith(notes): + fail_created_verification("notes") + if created_gate.get("isCompleted"): + fail_created_verification("isCompleted") + + mapping = { + "version": 1, + "list": config["list"], + "reminder_id": reminder_id, + "expected_title": preview["title"], + "required_notes_line": required_line, + "thread_id": thread_id, + "approval_meaning": intent["approval_meaning"], + "created_at": notification_at, + "request_id": request_identifier, + } + if not write_json( + state_dir / "gates" / f"{reminder_id}.json", mapping, exclusive=True + ): + write_json( + path, + { + **declared, + "status": "repair", + "reminder_id": reminder_id, + "repair": "stable reminder ID mapping already exists; inspect before retry", + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + raise ContractError("stable reminder ID mapping already exists; inspect before retry") + write_json( + path, + { + **declared, + "status": "gated", + "reminder_id": reminder_id, + "notification_at": notification_at, + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + request_lock_path.unlink() + return base_result( + "gated", + changed=True, + request_id=request_identifier, + reminder_id=reminder_id, + thread_id=thread_id, + notification_count=1, + next_safe_action="keep the owning task paused until exact completion delivery", + ) + + +def read_inventory( + config: dict[str, Any], *, timeout_seconds: float | None = None +) -> list[dict[str, Any]]: + """Read only the configured Apple Reminders list.""" + inventory = run_json( + [ + "remindctl", + "show", + "all", + "--list-id", + config["list"]["id"], + "--json", + "--no-input", + ], + timeout_seconds=timeout_seconds, + ) + if not isinstance(inventory, list): + raise ContractError("remindctl inventory must be a JSON array") + return inventory + + +def event_id(mapping: dict[str, Any]) -> str: + """Bind one approval event to its reminder, thread, and exact meaning.""" + payload = { + "approval_meaning": mapping["approval_meaning"], + "reminder_id": mapping["reminder_id"], + "thread_id": mapping["thread_id"], + } + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + return hashlib.sha256(encoded).hexdigest() + + +def validate_event_id(value: str) -> str: + """Require the exact lowercase SHA-256 receipt key shape.""" + if len(value) != 64 or any(character not in "0123456789abcdef" for character in value): + raise ContractError("event ID must be exactly 64 lowercase hexadecimal characters") + return value + + +def validate_event_binding( + value: Any, identifier: str, *, document_name: str +) -> dict[str, Any]: + """Reprove one claim or receipt against its exact event key.""" + required = ("approval_meaning", "event_id", "reminder_id", "thread_id") + if not isinstance(value, dict) or any( + not isinstance(value.get(field), str) or not value[field] for field in required + ): + raise ContractError(f"{document_name} lacks the exact event binding") + if value["event_id"] != identifier or event_id(value) != identifier: + raise ContractError(f"{document_name} does not match the exact event ID") + return value + + +def outcome_id(mapping: dict[str, Any], outcome: str, finished_at: str) -> str: + """Bind one terminal outcome receipt to its delivered approval event.""" + payload = { + "event_id": event_id(mapping), + "finished_at": finished_at, + "outcome": outcome, + } + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + return hashlib.sha256(encoded).hexdigest() + + +def validate_outcome(value: str) -> str: + """Accept one concise outcome line, never project history.""" + outcome = value.strip() + if not outcome or "\n" in outcome or "\r" in outcome or len(outcome) > 200: + raise ContractError("outcome must be one concise line of at most 200 characters") + return outcome + + +def validate_finished_at(value: str) -> str: + """Require an explicit timezone-aware terminal timestamp.""" + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as error: + raise ContractError("finished-at must be an ISO 8601 timestamp") from error + if parsed.tzinfo is None: + raise ContractError("finished-at must include a timezone") + return value + + +def append_outcome_notes(current_notes: str, addition: str) -> str: + """Append an outcome while preserving remindctl's managed URL footer.""" + lines = current_notes.rstrip().splitlines() + managed_url = lines[-1] if lines and lines[-1].startswith(MANAGED_URL_PREFIX) else None + if managed_url: + lines = lines[:-1] + body = "\n".join(lines).rstrip() + updated = f"{body}\n\n{addition}" if body else addition + if managed_url: + updated = f"{updated}\n\n{managed_url}" + return updated + + +def contains_outcome_notes(current_notes: str, addition: str) -> bool: + """Recognize one exact outcome block with an optional managed URL footer.""" + return current_notes == append_outcome_notes(current_notes.replace(addition, "").strip(), addition) + + +def read_gate_mapping(state_dir: Path, reminder_id: str) -> dict[str, Any]: + """Load only the mapping owned by one exact stable reminder ID.""" + path = state_dir / "gates" / f"{reminder_id}.json" + if not path.exists(): + raise ContractError("no gate mapping exists for the exact stable reminder ID") + mapping = load_json(path) + if mapping.get("reminder_id") != reminder_id: + raise ContractError("gate mapping stable reminder ID does not match") + for field in ("approval_meaning", "expected_title", "required_notes_line", "thread_id"): + if not mapping.get(field): + raise ContractError(f"gate mapping lacks required field: {field}") + return mapping + + +def validate_delivery_receipt( + receipt: dict[str, Any], mapping: dict[str, Any], identifier: str +) -> None: + """Require one receipt bound to the exact delivered approval contract.""" + expected = { + "approval_meaning": mapping["approval_meaning"], + "event_id": identifier, + "reminder_id": mapping["reminder_id"], + "thread_id": mapping["thread_id"], + } + for field, value in expected.items(): + if receipt.get(field) != value: + raise ContractError(f"delivery receipt does not match gate field: {field}") + if not receipt.get("delivered_at"): + raise ContractError("delivery receipt lacks delivered_at") + + +def read_exact_completed_reminder(reminder_id: str, list_id: str) -> dict[str, Any]: + """Resolve one stable ID inside the configured list's Completed view.""" + inventory = run_json( + [ + "remindctl", + "show", + "completed", + "--list-id", + list_id, + "--json", + "--no-input", + ] + ) + if not isinstance(inventory, list): + raise ContractError("completed reminder inventory must be a JSON array") + matches = [item for item in inventory if item.get("id") == reminder_id] + if len(matches) != 1: + raise ContractError("exact stable reminder ID did not resolve once in Completed history") + return matches[0] + + +def validate_outcome_target( + reminder: dict[str, Any], mapping: dict[str, Any], config: dict[str, Any] +) -> None: + """Reprove identity, meaning, list, and completed state before mutation.""" + if reminder.get("listID") != config["list"]["id"]: + raise ContractError("reminder resolved outside the configured list") + if mapping.get("list", {}).get("id") != config["list"]["id"]: + raise ContractError("gate mapping list does not match configuration") + if reminder.get("title") != mapping.get("expected_title"): + raise ContractError("reminder title changed; refusing outcome update") + if mapping.get("required_notes_line") not in (reminder.get("notes") or "").splitlines(): + raise ContractError("approval meaning is absent from reminder notes") + if not reminder.get("isCompleted") or not reminder.get("completionDate"): + raise ContractError("outcome requires an already completed reminder") + + +def record_outcome(args: argparse.Namespace) -> dict[str, Any]: + """Preview or append one bounded outcome to one delivered completed gate.""" + state_dir: Path = args.state_dir + outcome = validate_outcome(args.outcome) + finished_at = validate_finished_at(args.finished_at) + config = read_config(state_dir) + mapping = read_gate_mapping(state_dir, args.reminder_id) + delivery_id = event_id(mapping) + delivery_path = state_dir / "receipts" / f"{delivery_id}.json" + if not delivery_path.exists(): + raise ContractError("cannot record outcome without the delivery receipt") + validate_delivery_receipt(load_json(delivery_path), mapping, delivery_id) + + identifier = outcome_id(mapping, outcome, finished_at) + receipt_path = state_dir / "outcomes" / f"{delivery_id}.json" + if receipt_path.exists(): + receipt = load_json(receipt_path) + if receipt.get("outcome_id") != identifier: + raise ContractError("a different terminal outcome is already recorded for this gate") + update_request_state( + state_dir, + mapping, + "completed", + outcome_id=identifier, + finished_at=finished_at, + ) + return base_result( + "already_recorded", + changed=False, + outcome_id=identifier, + reminder_id=args.reminder_id, + ) + + addition = f"Outcome: {outcome}\nFinished: {finished_at}" + if not args.execute: + before = read_exact_completed_reminder(args.reminder_id, config["list"]["id"]) + validate_outcome_target(before, mapping, config) + return base_result( + "preview", + changed=False, + reminder_id=args.reminder_id, + append=addition, + side_effect="append outcome to exact completed Apple Reminder", + next_safe_action="review preview, then rerun with --execute", + ) + + claim_path = state_dir / "outcome-claims" / f"{delivery_id}.json" + claim = { + "event_id": delivery_id, + "finished_at": finished_at, + "outcome": outcome, + "outcome_id": identifier, + "reminder_id": args.reminder_id, + } + claim_created = write_json(claim_path, claim, exclusive=True) + if not claim_created: + existing_claim = load_json(claim_path) + if existing_claim.get("outcome_id") != identifier: + raise ContractError("a different terminal outcome claim already exists for this gate") + + before = read_exact_completed_reminder(args.reminder_id, config["list"]["id"]) + validate_outcome_target(before, mapping, config) + current_notes = before.get("notes") or "" + updated_notes = append_outcome_notes(current_notes, addition) + if contains_outcome_notes(current_notes, addition): + receipt = { + "event_id": delivery_id, + "finished_at": finished_at, + "outcome": outcome, + "outcome_id": identifier, + "recorded_at": datetime.now(timezone.utc).isoformat(), + "reminder_id": args.reminder_id, + "recovered": True, + } + if write_json(receipt_path, receipt, exclusive=True): + append_audit(state_dir / "outcome-audit.jsonl", receipt) + update_request_state( + state_dir, + mapping, + "completed", + outcome_id=identifier, + finished_at=finished_at, + ) + return base_result( + "already_recorded", + changed=False, + outcome_id=identifier, + reminder_id=args.reminder_id, + ) + + if not claim_created: + return base_result( + "claimed", + changed=False, + outcome_id=identifier, + reminder_id=args.reminder_id, + repair="inspect the exact reminder before releasing this outcome claim", + ) + + run_json( + [ + "remindctl", + "edit", + args.reminder_id, + "--notes", + updated_notes, + "--json", + "--no-input", + ] + ) + after = read_exact_completed_reminder(args.reminder_id, config["list"]["id"]) + validate_outcome_target(after, mapping, config) + if after.get("notes") != updated_notes: + raise ContractError("outcome notes failed exact post-update verification") + for key, value in before.items(): + if key not in {"notes", "lastModifiedDate"} and after.get(key) != value: + raise ContractError(f"unexpected reminder field changed: {key}") + + receipt = { + "event_id": delivery_id, + "finished_at": finished_at, + "outcome": outcome, + "outcome_id": identifier, + "recorded_at": datetime.now(timezone.utc).isoformat(), + "reminder_id": args.reminder_id, + "completion_date": after["completionDate"], + } + if not write_json(receipt_path, receipt, exclusive=True): + return base_result( + "already_recorded", + changed=False, + outcome_id=identifier, + reminder_id=args.reminder_id, + ) + append_audit(state_dir / "outcome-audit.jsonl", receipt) + update_request_state( + state_dir, + mapping, + "completed", + outcome_id=identifier, + finished_at=finished_at, + ) + return base_result( + "recorded", + changed=True, + outcome_id=identifier, + reminder_id=args.reminder_id, + ) + + +def poll(args: argparse.Namespace) -> dict[str, Any]: + """Claim at most one newly completed approval gate for task delivery.""" + state_dir: Path = args.state_dir + config = read_config(state_dir) + mapping_paths = sorted((state_dir / "gates").glob("*.json")) + if not mapping_paths: + return base_result("waiting", changed=False, open_gate_count=0) + inventory = read_inventory( + config, + timeout_seconds=getattr(args, "command_timeout_seconds", None), + ) + items_by_id = {item.get("id"): item for item in inventory} + + for mapping_path in mapping_paths: + mapping = load_json(mapping_path) + identifier = event_id(mapping) + receipt_path = state_dir / "receipts" / f"{identifier}.json" + if receipt_path.exists(): + continue + reminder = items_by_id.get(mapping.get("reminder_id")) + if not reminder: + raise ContractError("configured stable reminder ID did not resolve") + if reminder.get("listID") != config["list"]["id"]: + raise ContractError("reminder resolved outside the configured list") + if reminder.get("title") != mapping.get("expected_title"): + raise ContractError("reminder title changed; refusing semantic inference") + if mapping.get("required_notes_line") not in (reminder.get("notes") or "").splitlines(): + raise ContractError("approval meaning is absent from reminder notes") + if not reminder.get("isCompleted"): + continue + if not reminder.get("completionDate"): + raise ContractError("completed reminder lacks a completion timestamp") + + claim_path = state_dir / "claims" / f"{identifier}.json" + if claim_path.exists(): + return base_result( + "claimed", + changed=False, + event_id=identifier, + repair="inspect the destination task before releasing this claim", + ) + + claim = { + "claimed_at": datetime.now(timezone.utc).isoformat(), + "event_id": identifier, + "reminder_id": mapping["reminder_id"], + "completion_date": reminder["completionDate"], + "thread_id": mapping["thread_id"], + "approval_meaning": mapping["approval_meaning"], + } + if not write_json(claim_path, claim, exclusive=True): + return base_result("claimed", changed=False, event_id=identifier) + return base_result( + "deliver", + changed=True, + event_id=identifier, + completion_date=reminder["completionDate"], + thread_id=mapping["thread_id"], + prompt=( + f"Agent Attention approval received. {mapping['approval_meaning']} " + f"Receipt key: {identifier}. This approval applies only to that action." + ), + next_safe_action="deliver once with the Codex task tool, then record-delivery", + ) + + return base_result("waiting", changed=False, open_gate_count=len(mapping_paths)) + + +def watch(args: argparse.Namespace) -> dict[str, Any]: + """Wait in the foreground for one bounded completion detection window.""" + if args.interval_seconds <= 0 or args.interval_seconds > 15: + raise ContractError("interval-seconds must be greater than zero and at most 15") + if args.timeout_seconds <= 0 or args.timeout_seconds > 3600: + raise ContractError("timeout-seconds must be greater than zero and at most 3600") + started = time.monotonic() + deadline = started + args.timeout_seconds + polls = 0 + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + return base_result( + "waiting", + changed=False, + poll_count=polls, + watch_elapsed_seconds=round(time.monotonic() - started, 3), + ) + args.command_timeout_seconds = remaining + result = poll(args) + polls += 1 + if result["status"] in {"deliver", "claimed"}: + if result["status"] == "deliver": + completed = datetime.fromisoformat( + result["completion_date"].replace("Z", "+00:00") + ) + result["detection_latency_seconds"] = round( + max(0.0, (datetime.now(timezone.utc) - completed).total_seconds()), 3 + ) + result["poll_count"] = polls + result["watch_elapsed_seconds"] = round(time.monotonic() - started, 3) + return result + remaining = deadline - time.monotonic() + if remaining <= 0: + return base_result( + "waiting", + changed=False, + poll_count=polls, + watch_elapsed_seconds=round(time.monotonic() - started, 3), + ) + time.sleep(min(args.interval_seconds, remaining)) + + +def record_delivery(args: argparse.Namespace) -> dict[str, Any]: + """Record successful supported task delivery without reopening the reminder.""" + state_dir: Path = args.state_dir + identifier = validate_event_id(args.event_id) + claim_path = state_dir / "claims" / f"{identifier}.json" + receipt_path = state_dir / "receipts" / f"{identifier}.json" + if receipt_path.exists(): + receipt = validate_event_binding( + load_json(receipt_path), identifier, document_name="delivery receipt" + ) + update_request_state( + state_dir, + receipt, + "delivered", + event_id=identifier, + delivered_at=receipt.get("delivered_at"), + ) + return base_result("already_delivered", changed=False, event_id=identifier) + if not claim_path.exists(): + raise ContractError("cannot record delivery without an existing claim") + try: + tool_result = json.loads(args.tool_result) + except json.JSONDecodeError as error: + raise ContractError("tool result must be valid JSON") from error + if not isinstance(tool_result, dict) or tool_result.get("delivered") is not True: + raise ContractError("tool result does not confirm delivery") + + claim = validate_event_binding( + load_json(claim_path), identifier, document_name="delivery claim" + ) + receipt = { + **claim, + "delivered_at": datetime.now(timezone.utc).isoformat(), + "tool": "codex_app.send_message_to_thread", + "tool_result": tool_result, + } + if not write_json(receipt_path, receipt, exclusive=True): + return base_result("already_delivered", changed=False, event_id=identifier) + log_path = state_dir / "audit.jsonl" + log_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + with log_path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(receipt, sort_keys=True) + "\n") + os.chmod(log_path, 0o600) + update_request_state( + state_dir, + claim, + "delivered", + event_id=identifier, + delivered_at=receipt["delivered_at"], + ) + return base_result("recorded", changed=True, event_id=identifier) + + +def check_stop(args: argparse.Namespace) -> dict[str, Any]: + """Return a stop-hook decision from exact structured owner state only.""" + state_dir: Path = args.state_dir + thread_id = validate_thread_id(args.thread_id) + path = request_path(state_dir, thread_id) + if not path.exists(): + return base_result( + "clear", + changed=False, + thread_id=thread_id, + hook_action="allow", + ) + state = load_json(path) + if state.get("thread_id") != thread_id or state.get("version") != 1: + return base_result( + "repair_needed", + changed=False, + thread_id=thread_id, + hook_action="continue", + reason="Agent Attention owner state is malformed. Repair the exact request state before stopping.", + ) + status = state.get("status") + if status == "declared": + return base_result( + "repair_needed", + changed=False, + thread_id=thread_id, + hook_action="continue", + reason="Agent Attention blocker was declared but has no gate or repair result. Finish submit or record an actionable repair.", + ) + if status == "delivered": + continuation = state.get("intent", {}).get("continuation") + return base_result( + "resume_needed", + changed=False, + thread_id=thread_id, + hook_action="continue", + reason=f"Agent Attention approval was delivered. Resume: {continuation}", + ) + if status in {"gated", "repair", "completed"}: + values: dict[str, Any] = {} + if status == "repair": + values["repair"] = state.get("repair") + return base_result( + status, + changed=False, + thread_id=thread_id, + hook_action="allow", + **values, + ) + return base_result( + "repair_needed", + changed=False, + thread_id=thread_id, + hook_action="continue", + reason=f"Agent Attention owner state has unsupported status: {status}", + ) + + +def doctor(args: argparse.Namespace) -> dict[str, Any]: + """Report readiness without reading private message or reminder content.""" + state_dir: Path = args.state_dir + reminders = run_json(["remindctl", "doctor", "--for-agent", "--json"]) + config_path = state_dir / "config.json" + config_status: dict[str, Any] = {"configured": False} + if config_path.exists(): + config = read_config(state_dir) + config_status = {"configured": True, "list": config["list"]} + + handler_path = Path.home() / "Applications" / "Agent Attention Link.app" + info_path = handler_path / "Contents" / "Info.plist" + handler = {"installed": False, "path": str(handler_path)} + if info_path.exists(): + with info_path.open("rb") as handle: + info = plistlib.load(handle) + schemes = [ + scheme + for item in info.get("CFBundleURLTypes", []) + for scheme in item.get("CFBundleURLSchemes", []) + ] + handler["installed"] = "agent-attention" in schemes + + ready = bool(reminders.get("authorization", {}).get("authorized")) and config_status["configured"] and handler["installed"] + return base_result( + "ready" if ready else "repair_needed", + changed=False, + reminders={"authorized": reminders.get("authorization", {}).get("authorized", False)}, + config=config_status, + link_handler=handler, + next_safe_action=( + "submit or poll an approval gate" + if ready + else "configure the list and run install-link-handler.sh" + ), + ) + + +def commands(_: argparse.Namespace) -> dict[str, Any]: + """Expose the same command catalog used to build rendered help.""" + return base_result("ok", changed=False, commands=list(COMMAND_CATALOG)) + + +def parser() -> argparse.ArgumentParser: + """Build the stable command surface.""" + command = argparse.ArgumentParser( + prog="agent-attention", + description="Create and deliver bounded Apple Reminders approval gates.", + ) + command.add_argument("--state-dir", type=Path, default=default_state_dir()) + subcommands = command.add_subparsers(dest="command", required=True) + help_by_name = {item["name"]: item["summary"] for item in COMMAND_CATALOG} + + commands_command = subcommands.add_parser("commands", help=help_by_name["commands"]) + commands_command.set_defaults(handler=commands) + + doctor_command = subcommands.add_parser("doctor", help=help_by_name["doctor"]) + doctor_command.set_defaults(handler=doctor) + + configure_command = subcommands.add_parser("configure", help=help_by_name["configure"]) + configure_command.add_argument("--list-id", required=True) + configure_command.add_argument("--list-name", required=True) + configure_command.set_defaults(handler=configure) + + submit_command = subcommands.add_parser("submit", help=help_by_name["submit"]) + submit_command.add_argument("--thread-id", required=True, help="Exact owning Codex task UUID.") + submit_command.add_argument("--decision-type", required=True, help="Use yes_no only for an approvable gate.") + submit_command.add_argument("--unblocks-paused-task", action="store_true", help="Declare that the answer resumes paused work.") + submit_command.add_argument("--action", required=True, help="Short action shown in the reminder title.") + submit_command.add_argument("--recommendation", required=True, help="Recommendation-first decision guidance.") + submit_command.add_argument("--consequence", required=True, help="Bounded consequence of approval.") + submit_command.add_argument("--discussion-link", required=True, help="Exact agent-attention task link.") + submit_command.add_argument("--continuation", required=True, help="Exact work to resume after delivery.") + submit_command.add_argument("--approval-meaning", required=True, help="Sentence beginning with Approve.") + submit_command.add_argument("--execute", action="store_true", help="Create the admitted reminder and one alert.") + submit_command.set_defaults(handler=submit_approval) + + poll_command = subcommands.add_parser("poll", help=help_by_name["poll"]) + poll_command.set_defaults(handler=poll) + + watch_command = subcommands.add_parser("watch", help=help_by_name["watch"]) + watch_command.add_argument("--interval-seconds", type=float, default=5.0, help="Poll interval above 0 and at most 15 seconds.") + watch_command.add_argument("--timeout-seconds", type=float, default=30.0, help="Bounded foreground window above 0 and at most 3600 seconds.") + watch_command.set_defaults(handler=watch) + + record_command = subcommands.add_parser("record-delivery", help=help_by_name["record-delivery"]) + record_command.add_argument("--event-id", required=True) + record_command.add_argument("--tool-result", required=True) + record_command.set_defaults(handler=record_delivery) + + outcome_command = subcommands.add_parser("record-outcome", help=help_by_name["record-outcome"]) + outcome_command.add_argument("--reminder-id", required=True) + outcome_command.add_argument("--outcome", required=True) + outcome_command.add_argument("--finished-at", required=True) + outcome_command.add_argument("--execute", action="store_true") + outcome_command.set_defaults(handler=record_outcome) + + stop_command = subcommands.add_parser("check-stop", help=help_by_name["check-stop"]) + stop_command.add_argument("--thread-id", required=True, help="Exact Codex task UUID from the Stop event.") + stop_command.set_defaults(handler=check_stop) + return command + + +def main() -> int: + """Dispatch one command and emit one JSON result.""" + args = parser().parse_args() + try: + result = args.handler(args) + except (ContractError, json.JSONDecodeError, OSError, subprocess.SubprocessError) as error: + print(str(error), file=sys.stderr) + print( + json.dumps( + base_result( + "error", + changed="unknown", + retry_safe=False, + error_category="contract_or_runtime", + next_safe_action="inspect current state before retry", + ) + ) + ) + return 1 + print(json.dumps(result, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/experiments/agent-attention/runtime/agent-attention/install-link-handler.sh b/experiments/agent-attention/runtime/agent-attention/install-link-handler.sh new file mode 100755 index 0000000..edac428 --- /dev/null +++ b/experiments/agent-attention/runtime/agent-attention/install-link-handler.sh @@ -0,0 +1,16 @@ +#!/bin/zsh +set -euo pipefail + +script_dir=${0:A:h} +source_dir="$script_dir/link-handler" +app_dir="$HOME/Applications/Agent Attention Link.app" +contents_dir="$app_dir/Contents" +macos_dir="$contents_dir/MacOS" +register_bin="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister" + +mkdir -p "$macos_dir" +swiftc -parse-as-library "$source_dir/main.swift" -o "$macos_dir/AgentAttentionLink" +cp "$source_dir/Info.plist" "$contents_dir/Info.plist" +chmod 755 "$macos_dir/AgentAttentionLink" +"$register_bin" -f "$app_dir" +printf '{"status":"installed","app":"%s","scheme":"agent-attention"}\n' "$app_dir" diff --git a/experiments/agent-attention/runtime/agent-attention/link-handler/Info.plist b/experiments/agent-attention/runtime/agent-attention/link-handler/Info.plist new file mode 100644 index 0000000..418f6f1 --- /dev/null +++ b/experiments/agent-attention/runtime/agent-attention/link-handler/Info.plist @@ -0,0 +1,37 @@ + + + + + CFBundleDisplayName + Agent Attention Link + CFBundleExecutable + AgentAttentionLink + CFBundleIdentifier + local.nathanvale.AgentAttentionLink + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Agent Attention Link + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 1 + CFBundleURLTypes + + + CFBundleURLName + Agent Attention Thread + CFBundleURLSchemes + + agent-attention + + + + LSBackgroundOnly + + LSMinimumSystemVersion + 14.0 + + diff --git a/experiments/agent-attention/runtime/agent-attention/link-handler/main.swift b/experiments/agent-attention/runtime/agent-attention/link-handler/main.swift new file mode 100644 index 0000000..3836f91 --- /dev/null +++ b/experiments/agent-attention/runtime/agent-attention/link-handler/main.swift @@ -0,0 +1,87 @@ +import AppKit +import Foundation +import OSLog + +private let logger = Logger( + subsystem: "local.nathanvale.AgentAttentionLink", + category: "navigation" +) + +final class AppDelegate: NSObject, NSApplicationDelegate { + private var pendingTermination: DispatchWorkItem? + + func applicationDidFinishLaunching(_ notification: Notification) { + NSApp.setActivationPolicy(.prohibited) + + for argument in CommandLine.arguments.dropFirst() { + guard let url = URL(string: argument), url.scheme == "agent-attention" else { + continue + } + open(urls: [url]) + } + } + + func application(_ application: NSApplication, open urls: [URL]) { + open(urls: urls) + } + + private func open(urls: [URL]) { + guard let sourceURL = urls.last, + sourceURL.scheme == "agent-attention", + sourceURL.host == "threads" + else { + logger.error("Rejected malformed Agent Attention URL") + scheduleTermination() + return + } + + let pathParts = sourceURL.pathComponents.filter { $0 != "/" } + guard pathParts.count == 1, + let threadID = UUID(uuidString: pathParts[0]) + else { + logger.error("Rejected Agent Attention URL without one UUID thread ID") + scheduleTermination() + return + } + + guard let codexURL = URL(string: "codex://threads/\(threadID.uuidString.lowercased())") else { + logger.error("Could not construct Codex thread URL") + scheduleTermination() + return + } + + openCodex(codexURL) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) { + self.openCodex(codexURL) + } + scheduleTermination(after: 2.5) + } + + private func openCodex(_ url: URL) { + let configuration = NSWorkspace.OpenConfiguration() + configuration.activates = true + NSWorkspace.shared.open(url, configuration: configuration) { _, error in + if let error { + logger.error("Codex route open failed: \(error.localizedDescription, privacy: .public)") + } + } + } + + private func scheduleTermination(after delay: TimeInterval = 0.25) { + pendingTermination?.cancel() + let workItem = DispatchWorkItem { + NSApp.terminate(nil) + } + pendingTermination = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) + } +} +@main +enum AgentAttentionLink { + static func main() { + let application = NSApplication.shared + let delegate = AppDelegate() + application.delegate = delegate + application.run() + } +} diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py new file mode 100644 index 0000000..83f0796 --- /dev/null +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -0,0 +1,729 @@ +#!/usr/bin/env python3 +"""Public-command tests for Agent Attention runtime contracts.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +import time +from datetime import datetime, timezone +import unittest +from pathlib import Path +from typing import Any + + +RUNTIME = Path(__file__).with_name("agent-attention.py") +LIST_ID = "32C46BA9-FE7A-4758-AA9E-4C4A249A5DF6" +REMINDER_ID = "11111111-1111-4111-8111-111111111111" +OTHER_REMINDER_ID = "22222222-2222-4222-8222-222222222222" +NEW_REMINDER_ID = "33333333-3333-4333-8333-333333333333" +THREAD_ID = "019fc54e-ff95-7ca1-af49-5720c36fdc0d" +APPROVAL_MEANING = "Approve the bounded outcome-receipt test only." +FINISHED_AT = "2026-08-10T05:45:00Z" + + +class AgentAttentionCommandTest(unittest.TestCase): + """Prove discovery and outcome behavior through the public CLI.""" + + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.state_dir = self.root / "state" + self.bin_dir = self.root / "bin" + self.inventory_path = self.root / "inventory.json" + self.calls_path = self.root / "calls.jsonl" + self.poll_count_path = self.root / "poll-count.txt" + self.bin_dir.mkdir() + self._write_fake_remindctl() + self.env = { + **os.environ, + "PATH": f"{self.bin_dir}:{os.environ['PATH']}", + "FAKE_REMINDERS_INVENTORY": str(self.inventory_path), + "FAKE_REMINDERS_CALLS": str(self.calls_path), + "FAKE_REMINDERS_POLL_COUNT": str(self.poll_count_path), + } + self.mapping = { + "version": 1, + "list": {"id": LIST_ID, "name": "Agent Attention"}, + "reminder_id": REMINDER_ID, + "expected_title": "[APPROVE] Approve outcome receipt test", + "required_notes_line": f"Approval meaning: {APPROVAL_MEANING}", + "thread_id": THREAD_ID, + "approval_meaning": APPROVAL_MEANING, + } + self.target = { + "id": REMINDER_ID, + "listID": LIST_ID, + "listName": "Agent Attention", + "title": self.mapping["expected_title"], + "notes": ( + "Recommended: approve.\n\n" + f"Approval meaning: {APPROVAL_MEANING}\n" + "Tick = approve only this action.\n" + "Discuss or disagree: open Codex.\n\n" + f"remindctl URL (managed): agent-attention://threads/{THREAD_ID}" + ), + "url": f"agent-attention://threads/{THREAD_ID}", + "priority": "high", + "isCompleted": True, + "completionDate": "2026-08-10T05:40:00Z", + "lastModifiedDate": "2026-08-10T05:40:00Z", + } + self.other = { + "id": OTHER_REMINDER_ID, + "listID": LIST_ID, + "listName": "Agent Attention", + "title": "[APPROVE] Other gate", + "notes": "Unrelated sentinel", + "priority": "low", + "isCompleted": False, + "lastModifiedDate": "2026-08-10T05:30:00Z", + } + self._write_state(delivery_receipt=True) + self._write_inventory() + + def tearDown(self) -> None: + self.temporary.cleanup() + + def _write_fake_remindctl(self) -> None: + path = self.bin_dir / "remindctl" + path.write_text( + """#!/usr/bin/env python3 +import json +import os +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +inventory_path = Path(os.environ["FAKE_REMINDERS_INVENTORY"]) +calls_path = Path(os.environ["FAKE_REMINDERS_CALLS"]) +args = sys.argv[1:] +with calls_path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(args) + "\\n") +inventory = json.loads(inventory_path.read_text(encoding="utf-8")) + +if args[0] == "show": + if os.environ.get("FAKE_REMINDERS_HANG_SHOW_SECONDS"): + time.sleep(float(os.environ["FAKE_REMINDERS_HANG_SHOW_SECONDS"])) + if args[1] == "all" and os.environ.get("FAKE_REMINDERS_COMPLETE_AFTER_POLLS"): + poll_count_path = Path(os.environ["FAKE_REMINDERS_POLL_COUNT"]) + count = int(poll_count_path.read_text() if poll_count_path.exists() else "0") + 1 + poll_count_path.write_text(str(count)) + if count >= int(os.environ["FAKE_REMINDERS_COMPLETE_AFTER_POLLS"]): + target = inventory[0] + if not target.get("isCompleted"): + target["isCompleted"] = True + target["completionDate"] = datetime.now(timezone.utc).isoformat() + inventory_path.write_text(json.dumps(inventory), encoding="utf-8") + if args[1] == "completed": + inventory = [item for item in inventory if item.get("isCompleted")] + print(json.dumps(inventory)) +elif args[0] == "add": + reminder = { + "id": os.environ.get("FAKE_REMINDERS_NEW_ID", "33333333-3333-4333-8333-333333333333"), + "listID": args[args.index("--list-id") + 1], + "listName": "Agent Attention", + "title": args[args.index("--title") + 1], + "notes": args[args.index("--notes") + 1], + "url": args[args.index("--url") + 1], + "priority": args[args.index("--priority") + 1], + "alarm": args[args.index("--alarm") + 1], + "isCompleted": False, + "lastModifiedDate": datetime.now(timezone.utc).isoformat(), + } + inventory.append(reminder) + inventory_path.write_text(json.dumps(inventory), encoding="utf-8") + print(json.dumps(reminder)) +elif args[0] == "edit": + reminder_id = args[1] + notes = args[args.index("--notes") + 1] + matches = [item for item in inventory if item.get("id") == reminder_id] + if len(matches) != 1: + raise SystemExit(4) + matches[0]["notes"] = notes + matches[0]["lastModifiedDate"] = "2026-08-10T05:45:01Z" + inventory_path.write_text(json.dumps(inventory), encoding="utf-8") + if os.environ.get("FAKE_REMINDERS_FAIL_AFTER_EDIT") == "1": + print("unknown result after write", file=sys.stderr) + raise SystemExit(5) + print(json.dumps(matches[0])) +else: + print(json.dumps({"authorization": {"authorized": True}})) +""", + encoding="utf-8", + ) + path.chmod(0o755) + + def _write_inventory(self) -> None: + self.inventory_path.write_text( + json.dumps([self.target, self.other]), encoding="utf-8" + ) + + def _write_state(self, *, delivery_receipt: bool) -> None: + (self.state_dir / "gates").mkdir(parents=True) + (self.state_dir / "config.json").write_text( + json.dumps({"version": 1, "list": self.mapping["list"]}), + encoding="utf-8", + ) + (self.state_dir / "gates" / f"{REMINDER_ID}.json").write_text( + json.dumps(self.mapping), encoding="utf-8" + ) + if delivery_receipt: + identifier = self._event_id() + (self.state_dir / "receipts").mkdir() + (self.state_dir / "receipts" / f"{identifier}.json").write_text( + json.dumps( + { + "event_id": identifier, + "reminder_id": REMINDER_ID, + "thread_id": THREAD_ID, + "approval_meaning": APPROVAL_MEANING, + "delivered_at": "2026-08-10T05:41:00Z", + } + ), + encoding="utf-8", + ) + + def _event_id(self) -> str: + import hashlib + + payload = { + "approval_meaning": APPROVAL_MEANING, + "reminder_id": REMINDER_ID, + "thread_id": THREAD_ID, + } + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + return hashlib.sha256(encoded).hexdigest() + + def _outcome_id(self, outcome: str) -> str: + import hashlib + + payload = { + "event_id": self._event_id(), + "finished_at": FINISHED_AT, + "outcome": outcome, + } + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + return hashlib.sha256(encoded).hexdigest() + + def run_cli( + self, *arguments: str, env_update: dict[str, str] | None = None + ) -> subprocess.CompletedProcess[str]: + env = {**self.env, **(env_update or {})} + return subprocess.run( + [ + sys.executable, + str(RUNTIME), + "--state-dir", + str(self.state_dir), + *arguments, + ], + capture_output=True, + text=True, + env=env, + ) + + def result(self, completed: subprocess.CompletedProcess[str]) -> dict[str, Any]: + self.assertEqual(completed.returncode, 0, completed.stderr) + return json.loads(completed.stdout) + + def calls(self) -> list[list[str]]: + if not self.calls_path.exists(): + return [] + return [json.loads(line) for line in self.calls_path.read_text().splitlines()] + + def submit_arguments(self, **overrides: str | bool) -> tuple[str, ...]: + values: dict[str, str | bool] = { + "decision_type": "yes_no", + "unblocks_paused_task": True, + "action": "Approve local router test", + "recommendation": "Approve the bounded local router test.", + "consequence": "The paused task will resume and run its focused tests.", + "discussion_link": f"agent-attention://threads/{THREAD_ID}", + "continuation": "Run the focused tests, then record the terminal outcome.", + "approval_meaning": "Approve the bounded local router test only.", + } + values.update(overrides) + arguments = ["submit", "--thread-id", THREAD_ID] + for field, value in values.items(): + flag = f"--{field.replace('_', '-')}" + if isinstance(value, bool): + if value: + arguments.append(flag) + else: + arguments.extend([flag, value]) + return tuple(arguments) + + def test_command_discovery_help_and_parser_stay_aligned(self) -> None: + discovery = self.result(self.run_cli("commands")) + command_names = [item["name"] for item in discovery["commands"]] + self.assertIn("record-outcome", command_names) + self.assertNotIn("create", command_names) + + help_result = self.run_cli("--help") + self.assertEqual(help_result.returncode, 0) + for command_name in command_names: + self.assertIn(command_name, help_result.stdout) + self.assertEqual(self.run_cli(command_name, "--help").returncode, 0) + self.assertNotEqual(self.run_cli("create", "--help").returncode, 0) + submit_help = self.run_cli("submit", "--help").stdout + for term in ("yes_no", "paused", "Approve", "one alert"): + self.assertIn(term, submit_help) + + def test_outcome_preview_is_read_only_and_exact(self) -> None: + result = self.result( + self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Review-ready PR opened; local checks passed.", + "--finished-at", + FINISHED_AT, + ) + ) + self.assertEqual(result["status"], "preview") + self.assertFalse(result["changed"]) + self.assertEqual(result["reminder_id"], REMINDER_ID) + self.assertEqual( + self.calls(), + [ + [ + "show", + "completed", + "--list-id", + LIST_ID, + "--json", + "--no-input", + ] + ], + ) + self.assertNotIn("Outcome:", self.target["notes"]) + + def test_outcome_execute_updates_only_exact_completed_gate_once(self) -> None: + arguments = ( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Review-ready PR opened; local checks passed.", + "--finished-at", + FINISHED_AT, + "--execute", + ) + first = self.result(self.run_cli(*arguments)) + self.assertEqual(first["status"], "recorded") + self.assertTrue(first["changed"]) + inventory = json.loads(self.inventory_path.read_text()) + target = next(item for item in inventory if item["id"] == REMINDER_ID) + other = next(item for item in inventory if item["id"] == OTHER_REMINDER_ID) + self.assertTrue(target["isCompleted"]) + self.assertEqual(target["completionDate"], "2026-08-10T05:40:00Z") + self.assertIn( + ( + "Outcome: Review-ready PR opened; local checks passed.\n" + f"Finished: {FINISHED_AT}" + ), + target["notes"], + ) + self.assertTrue( + target["notes"].endswith( + f"remindctl URL (managed): agent-attention://threads/{THREAD_ID}" + ) + ) + self.assertEqual(other, self.other) + outcome_receipts = list((self.state_dir / "outcomes").glob("*.json")) + self.assertEqual(len(outcome_receipts), 1) + audit_lines = (self.state_dir / "outcome-audit.jsonl").read_text().splitlines() + self.assertEqual(len(audit_lines), 1) + self.assertEqual(json.loads(audit_lines[0]), json.loads(outcome_receipts[0].read_text())) + self.assertEqual( + self.calls(), + [ + ["show", "completed", "--list-id", LIST_ID, "--json", "--no-input"], + ["edit", REMINDER_ID, "--notes", target["notes"], "--json", "--no-input"], + ["show", "completed", "--list-id", LIST_ID, "--json", "--no-input"], + ], + ) + + second = self.result(self.run_cli(*arguments)) + self.assertEqual(second["status"], "already_recorded") + self.assertFalse(second["changed"]) + self.assertEqual(len([call for call in self.calls() if call[0] == "edit"]), 1) + self.assertEqual( + len((self.state_dir / "outcome-audit.jsonl").read_text().splitlines()), 1 + ) + + def test_outcome_rejects_a_second_terminal_result(self) -> None: + first = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "First terminal result.", + "--finished-at", + FINISHED_AT, + "--execute", + ) + self.assertEqual(first.returncode, 0, first.stderr) + second = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Different terminal result.", + "--finished-at", + FINISHED_AT, + "--execute", + ) + self.assertEqual(second.returncode, 1) + self.assertIn("different terminal outcome", second.stderr) + self.assertEqual(len([call for call in self.calls() if call[0] == "edit"]), 1) + + def test_outcome_unknown_result_recovers_by_exact_reread_without_second_edit(self) -> None: + arguments = ( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Recovered terminal result.", + "--finished-at", + FINISHED_AT, + "--execute", + ) + unknown = self.run_cli( + *arguments, env_update={"FAKE_REMINDERS_FAIL_AFTER_EDIT": "1"} + ) + self.assertEqual(unknown.returncode, 1) + self.assertIn("inspect current state before retry", unknown.stdout) + + recovered = self.result(self.run_cli(*arguments)) + self.assertEqual(recovered["status"], "already_recorded") + self.assertEqual(len([call for call in self.calls() if call[0] == "edit"]), 1) + receipt = json.loads(next((self.state_dir / "outcomes").glob("*.json")).read_text()) + self.assertTrue(receipt["recovered"]) + + def test_existing_outcome_claim_suppresses_a_concurrent_edit(self) -> None: + outcome = "Concurrent terminal result." + claim_dir = self.state_dir / "outcome-claims" + claim_dir.mkdir() + (claim_dir / f"{self._event_id()}.json").write_text( + json.dumps( + { + "event_id": self._event_id(), + "finished_at": FINISHED_AT, + "outcome": outcome, + "outcome_id": self._outcome_id(outcome), + "reminder_id": REMINDER_ID, + } + ), + encoding="utf-8", + ) + result = self.result( + self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + outcome, + "--finished-at", + FINISHED_AT, + "--execute", + ) + ) + self.assertEqual(result["status"], "claimed") + self.assertEqual( + self.calls(), + [["show", "completed", "--list-id", LIST_ID, "--json", "--no-input"]], + ) + + def test_outcome_rejects_incomplete_gate(self) -> None: + self.target["isCompleted"] = False + self.target.pop("completionDate") + self._write_inventory() + completed = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Should fail.", + "--finished-at", + FINISHED_AT, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("Completed history", completed.stderr) + + def test_outcome_rejects_missing_delivery_receipt(self) -> None: + for path in (self.state_dir / "receipts").glob("*.json"): + path.unlink() + completed = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Should fail.", + "--finished-at", + FINISHED_AT, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("delivery receipt", completed.stderr) + + def test_outcome_rejects_delivery_receipt_for_a_different_task(self) -> None: + receipt_path = next((self.state_dir / "receipts").glob("*.json")) + receipt = json.loads(receipt_path.read_text()) + receipt["thread_id"] = "33333333-3333-4333-8333-333333333333" + receipt_path.write_text(json.dumps(receipt), encoding="utf-8") + completed = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Should fail.", + "--finished-at", + FINISHED_AT, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("does not match gate field: thread_id", completed.stderr) + self.assertEqual(self.calls(), []) + + def test_outcome_rejects_multiline_history(self) -> None: + completed = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "First line.\nSecond line.", + "--finished-at", + FINISHED_AT, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("one concise line", completed.stderr) + self.assertEqual(self.calls(), []) + + def test_submit_admits_one_gate_with_one_alert_and_suppresses_duplicate(self) -> None: + preview = self.result(self.run_cli(*self.submit_arguments())) + self.assertEqual(preview["status"], "admitted_preview") + self.assertFalse(preview["changed"]) + self.assertEqual(self.calls(), []) + self.assertEqual(preview["preview"]["priority"], "none") + + arguments = (*self.submit_arguments(), "--execute") + first = self.result(self.run_cli(*arguments)) + self.assertEqual(first["status"], "gated") + self.assertEqual(first["notification_count"], 1) + add_calls = [call for call in self.calls() if call[0] == "add"] + self.assertEqual(len(add_calls), 1) + self.assertIn("--alarm", add_calls[0]) + self.assertNotIn("--due", add_calls[0]) + self.assertEqual(add_calls[0][add_calls[0].index("--priority") + 1], "none") + inventory = json.loads(self.inventory_path.read_text()) + self.assertEqual(next(item for item in inventory if item["id"] == REMINDER_ID), self.target) + self.assertEqual(next(item for item in inventory if item["id"] == OTHER_REMINDER_ID), self.other) + self.assertEqual(len([item for item in inventory if item["id"] == NEW_REMINDER_ID]), 1) + + second = self.result(self.run_cli(*arguments)) + self.assertEqual(second["status"], "already_gated") + self.assertFalse(second["changed"]) + self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + + def test_completed_task_may_open_one_later_distinct_gate(self) -> None: + first = self.result( + self.run_cli(*self.submit_arguments(), "--execute") + ) + request_path = self.state_dir / "requests" / f"{THREAD_ID}.json" + request = json.loads(request_path.read_text()) + request["status"] = "completed" + request_path.write_text(json.dumps(request), encoding="utf-8") + + second = self.result( + self.run_cli( + *self.submit_arguments( + action="Approve later local router test", + recommendation="Approve the later bounded local router test.", + approval_meaning="Approve the later bounded local router test only.", + ), + "--execute", + env_update={ + "FAKE_REMINDERS_NEW_ID": "44444444-4444-4444-8444-444444444444" + }, + ) + ) + + self.assertEqual(first["status"], "gated") + self.assertEqual(second["status"], "gated") + self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 2) + + def test_submit_rejects_multi_choice_and_records_actionable_repair(self) -> None: + result = self.result( + self.run_cli( + *self.submit_arguments(decision_type="multi_choice"), + "--execute", + ) + ) + self.assertEqual(result["status"], "rejected") + self.assertIn("multi-choice", result["repair"]) + self.assertEqual(self.calls(), []) + stop = self.result(self.run_cli("check-stop", "--thread-id", THREAD_ID)) + self.assertEqual(stop["status"], "repair") + self.assertEqual(stop["hook_action"], "allow") + + def test_submit_rejects_information_updates_and_non_blockers(self) -> None: + for overrides, expected in ( + ({"decision_type": "information"}, "decision_type"), + ({"unblocks_paused_task": False}, "paused owning task"), + ({"discussion_link": "agent-attention://threads/wrong"}, "exact owning"), + ): + with self.subTest(overrides=overrides): + result = self.result(self.run_cli(*self.submit_arguments(**overrides))) + self.assertEqual(result["status"], "rejected") + self.assertIn(expected, result["repair"]) + self.assertEqual(self.calls(), []) + + def test_existing_submit_claim_suppresses_a_concurrent_gate_and_alert(self) -> None: + claim_dir = self.state_dir / "request-locks" + claim_dir.mkdir() + (claim_dir / f"{THREAD_ID}.json").write_text( + json.dumps({"thread_id": THREAD_ID, "request_id": "winner"}), + encoding="utf-8", + ) + result = self.result( + self.run_cli(*self.submit_arguments(), "--execute") + ) + self.assertEqual(result["status"], "claimed") + self.assertFalse(result["changed"]) + self.assertEqual(self.calls(), []) + + def test_stop_check_continues_declared_or_delivered_state_without_reading_prose(self) -> None: + request_dir = self.state_dir / "requests" + request_dir.mkdir() + path = request_dir / f"{THREAD_ID}.json" + path.write_text( + json.dumps( + { + "version": 1, + "thread_id": THREAD_ID, + "status": "declared", + "intent": {"continuation": "Run the exact continuation."}, + } + ), + encoding="utf-8", + ) + declared = self.result(self.run_cli("check-stop", "--thread-id", THREAD_ID)) + self.assertEqual(declared["hook_action"], "continue") + self.assertNotIn("assistant", json.dumps(declared).casefold()) + + state = json.loads(path.read_text()) + state["status"] = "delivered" + path.write_text(json.dumps(state), encoding="utf-8") + delivered = self.result(self.run_cli("check-stop", "--thread-id", THREAD_ID)) + self.assertEqual(delivered["hook_action"], "continue") + self.assertIn("Run the exact continuation", delivered["reason"]) + + def test_watch_detects_and_records_delivery_under_fifteen_seconds(self) -> None: + self.target["isCompleted"] = False + self.target.pop("completionDate") + self._write_inventory() + for path in (self.state_dir / "receipts").glob("*.json"): + path.unlink() + result = self.result( + self.run_cli( + "watch", + "--interval-seconds", + "0.05", + "--timeout-seconds", + "1", + env_update={"FAKE_REMINDERS_COMPLETE_AFTER_POLLS": "2"}, + ) + ) + self.assertEqual(result["status"], "deliver") + self.assertEqual(result["poll_count"], 2) + self.assertLess(result["detection_latency_seconds"], 15) + recorded = self.result( + self.run_cli( + "record-delivery", + "--event-id", + result["event_id"], + "--tool-result", + '{"delivered":true,"fixture":"exact-task"}', + ) + ) + self.assertEqual(recorded["status"], "recorded") + completion = datetime.fromisoformat(result["completion_date"]) + self.assertLess((datetime.now(timezone.utc) - completion).total_seconds(), 15) + duplicate = self.result(self.run_cli("poll")) + self.assertEqual(duplicate["status"], "waiting") + self.assertEqual( + len((self.state_dir / "audit.jsonl").read_text().splitlines()), 1 + ) + + def test_watch_bounds_a_hung_remindctl_call(self) -> None: + started = time.monotonic() + completed = self.run_cli( + "watch", + "--interval-seconds", + "0.05", + "--timeout-seconds", + "0.2", + env_update={"FAKE_REMINDERS_HANG_SHOW_SECONDS": "2"}, + ) + self.assertEqual(completed.returncode, 1) + self.assertLess(time.monotonic() - started, 1) + self.assertIn("bounded execution window", completed.stderr) + + def test_record_delivery_rejects_invalid_event_ids_and_non_boolean_proof(self) -> None: + for invalid_id in ("../requests/forged", "A" * 64, "0" * 63): + with self.subTest(event_id=invalid_id): + completed = self.run_cli( + "record-delivery", + "--event-id", + invalid_id, + "--tool-result", + '{"delivered":true}', + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("64 lowercase hexadecimal", completed.stderr) + + claim_dir = self.state_dir / "claims" + claim_dir.mkdir() + identifier = self._event_id() + (self.state_dir / "receipts" / f"{identifier}.json").unlink() + (claim_dir / f"{identifier}.json").write_text( + json.dumps( + { + "event_id": identifier, + "reminder_id": REMINDER_ID, + "thread_id": THREAD_ID, + "approval_meaning": APPROVAL_MEANING, + } + ), + encoding="utf-8", + ) + for tool_result in ('{"delivered":"false"}', '{"delivered":1}', '[]'): + with self.subTest(tool_result=tool_result): + completed = self.run_cli( + "record-delivery", + "--event-id", + identifier, + "--tool-result", + tool_result, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("does not confirm delivery", completed.stderr) + + def test_delivered_historical_gate_may_be_human_deleted_without_blocking_poll(self) -> None: + self.inventory_path.write_text(json.dumps([self.other]), encoding="utf-8") + result = self.result(self.run_cli("poll")) + self.assertEqual(result["status"], "waiting") + self.assertFalse(result["changed"]) + self.assertEqual( + self.calls(), + [["show", "all", "--list-id", LIST_ID, "--json", "--no-input"]], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/experiments/agent-attention/skill/SKILL.md b/experiments/agent-attention/skill/SKILL.md new file mode 100644 index 0000000..3f2a5d6 --- /dev/null +++ b/experiments/agent-attention/skill/SKILL.md @@ -0,0 +1,36 @@ +--- +name: agent-attention +description: "Route a genuine yes/no approval blocker from a paused Codex task into Apple Reminders." +--- + +# Agent Attention + +Use only when one explicit yes/no approval blocks the current Codex task. +Keep discussion, disagreement, multi-choice, and unclear requests in Codex. + +## Owner + +`experiments/agent-attention/runtime/agent-attention/agent-attention.py` 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`. +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 + are the expected side effects. +4. If rejected, follow the returned repair or keep the decision in Codex. +5. When gated, leave the task paused. No response means no approval. +6. After exact-task delivery, apply only the stated approval meaning, run the + continuation, then use the owner’s outcome command. + +Never infer approval from prose. Never create a second gate for the same +request. Never delete or reopen the completed reminder. + +## Next safe action + +Start with the `submit` preview. Stop on owner-state repair, missing EventKit +access, or an exact Reminders approval gate. diff --git a/package.json b/package.json index fe51500..11e2a6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-plugin-template-tooling", - "version": "0.3.0", + "version": "0.1.0", "private": true, "type": "module", "packageManager": "bun@1.3.14", @@ -9,6 +9,7 @@ ], "scripts": { "test": "bun test", + "test:agent-attention": "python3 -m unittest experiments/agent-attention/runtime/agent-attention/test_agent_attention.py && bun test experiments/agent-attention/hooks/agent-attention-codex-stop.test.ts", "init": "bun run scripts/init.ts", "generate": "bun run scripts/generate.ts", "generate:check": "bun run scripts/generate.ts --check", diff --git a/plugin.config.json b/plugin.config.json index 04e8318..0bdd0c1 100644 --- a/plugin.config.json +++ b/plugin.config.json @@ -1,21 +1,21 @@ { - "template": true, - "name": "harness-native-plugin-prototype", - "displayName": "Harness Plugin Prototype", - "version": "0.3.0", - "description": "Portable Bun skills and a guided native capability tour for Claude Code and Codex", + "template": false, + "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", "author": { - "name": "Prototype" + "name": "My Agent Dojo" }, - "repository": "https://github.com/myagentdojo/agent-plugin-template", + "repository": "https://github.com/myagentdojo/agent-plugin-playground", "license": "MIT", "keywords": [ "agent-plugin", "bun" ], "category": "Developer Tools", - "shortDescription": "Tour native plugin features", - "longDescription": "Run a guided capability tour across Claude Code and Codex, inspect native lifecycle declarations, and keep using dependency-closed skills through one verified, plugin-managed Bun runtime.", + "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.", "capabilities": [ "Execute verified Bun code", "Download Bun after approval", @@ -34,7 +34,7 @@ "canary": { "owner": "myagentdojo", "actor": "myagentdojo", - "publicRepository": "agent-plugin-template-canary-public", - "privateRepository": "agent-plugin-template-canary-private" + "publicRepository": "agent-plugin-playground-public-canary", + "privateRepository": "agent-plugin-playground-private-canary" } } diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index f4919fb..f688f5f 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,13 +1,13 @@ { - "name": "harness-native-plugin-prototype", - "displayName": "Harness Plugin Prototype", - "version": "0.3.0", + "name": "agent-plugin-playground", + "displayName": "Agent Plugin Playground", + "version": "0.1.0", "defaultEnabled": false, - "description": "Portable Bun skills and a guided native capability tour for Claude Code and Codex", + "description": "Bun-powered native plugin playground for one active experiment at a time", "author": { - "name": "Prototype" + "name": "My Agent Dojo" }, - "repository": "https://github.com/myagentdojo/agent-plugin-template", + "repository": "https://github.com/myagentdojo/agent-plugin-playground", "license": "MIT", "keywords": [ "agent-plugin", diff --git a/plugin/.codex-plugin/plugin.json b/plugin/.codex-plugin/plugin.json index 6ed293a..6497152 100644 --- a/plugin/.codex-plugin/plugin.json +++ b/plugin/.codex-plugin/plugin.json @@ -1,11 +1,11 @@ { - "name": "harness-native-plugin-prototype", - "version": "0.3.0", - "description": "Portable Bun skills and a guided native capability tour for Claude Code and Codex", + "name": "agent-plugin-playground", + "version": "0.1.0", + "description": "Bun-powered native plugin playground for one active experiment at a time", "author": { - "name": "Prototype" + "name": "My Agent Dojo" }, - "repository": "https://github.com/myagentdojo/agent-plugin-template", + "repository": "https://github.com/myagentdojo/agent-plugin-playground", "license": "MIT", "keywords": [ "agent-plugin", @@ -14,10 +14,10 @@ "skills": "./skills/", "hooks": "./hooks/codex/hooks.json", "interface": { - "displayName": "Harness Plugin Prototype", - "shortDescription": "Tour native plugin features", - "longDescription": "Run a guided capability tour across Claude Code and Codex, inspect native lifecycle declarations, and keep using dependency-closed skills through one verified, plugin-managed Bun runtime.", - "developerName": "Prototype", + "displayName": "Agent Plugin Playground", + "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.", + "developerName": "My Agent Dojo", "category": "Developer Tools", "capabilities": [ "Execute verified Bun code", diff --git a/plugin/hooks/fixture/lifecycle-mechanics-proof.generated.json b/plugin/hooks/fixture/lifecycle-mechanics-proof.generated.json index 9ea8b3b..9953eba 100644 --- a/plugin/hooks/fixture/lifecycle-mechanics-proof.generated.json +++ b/plugin/hooks/fixture/lifecycle-mechanics-proof.generated.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "purpose": "Harness Plugin Prototype lifecycle mechanics proof" + "purpose": "Agent Plugin Playground lifecycle mechanics proof" } diff --git a/plugin/hooks/fixture/lifecycle-mechanics-proof.source.json b/plugin/hooks/fixture/lifecycle-mechanics-proof.source.json index 9ea8b3b..9953eba 100644 --- a/plugin/hooks/fixture/lifecycle-mechanics-proof.source.json +++ b/plugin/hooks/fixture/lifecycle-mechanics-proof.source.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "purpose": "Harness Plugin Prototype lifecycle mechanics proof" + "purpose": "Agent Plugin Playground lifecycle mechanics proof" } diff --git a/scripts/native-capability-hook.test.ts b/scripts/native-capability-hook.test.ts index 90b29cb..a4e6aa4 100644 --- a/scripts/native-capability-hook.test.ts +++ b/scripts/native-capability-hook.test.ts @@ -593,7 +593,7 @@ test("generation owns one deterministic LF fixture projection", () => { "lifecycle-mechanics-proof.generated.json", ) const expected = - '{\n "schemaVersion": 1,\n "purpose": "Harness Plugin Prototype lifecycle mechanics proof"\n}\n' + '{\n "schemaVersion": 1,\n "purpose": "Agent Plugin Playground lifecycle mechanics proof"\n}\n' expect(readFileSync(sourcePath, "utf8")).toBe(expected) expect(readFileSync(projectionPath, "utf8")).toBe(expected) expect(runGenerateCheck(temporaryRepository).exitCode).toBe(0) diff --git a/scripts/native-capability-surface.test.ts b/scripts/native-capability-surface.test.ts index 34336ba..1a670e1 100644 --- a/scripts/native-capability-surface.test.ts +++ b/scripts/native-capability-surface.test.ts @@ -86,7 +86,7 @@ test("checked-in manifests expose one coherent tour identity and relative native readFileSync(join(root, "plugin", ".codex-plugin", "plugin.json"), "utf8"), ) - expect(config.name).toBe("harness-native-plugin-prototype") + expect(config.name).toBe("agent-plugin-playground") expect(config.defaultPrompts).toEqual(["Run the native plugin capability tour."]) expect(claudeManifest).toMatchObject({ name: config.name, diff --git a/scripts/prove-harness-install.test.ts b/scripts/prove-harness-install.test.ts index d13160a..7ff9afa 100644 --- a/scripts/prove-harness-install.test.ts +++ b/scripts/prove-harness-install.test.ts @@ -784,7 +784,7 @@ test("managed or non-removable Codex state blocks with administrator handoff", ( codexNativeTest("Codex JSON records native state (Codex CLI required; fallback proves bytes)", () => { expect(proof.codex.mode).toBe("native-local-marketplace") - expect(proof.codex.marketplaceIdentity).toBe("harness-native-plugin-prototype") + expect(proof.codex.marketplaceIdentity).toBe("agent-plugin-playground") expect(proof.codex.configuredRef).toBe(proof.preflight.requestedRef) expect(proof.codex.installedMarketplaceRoot).toBeTruthy() expect(proof.codex.installedPath).toBeTruthy() From f909620ab2529f0fa0fd907cd10c00e540791ff8 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Mon, 10 Aug 2026 17:17:04 +1000 Subject: [PATCH 02/24] docs: clarify Agent Attention runtime boundary --- experiments/agent-attention/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/experiments/agent-attention/README.md b/experiments/agent-attention/README.md index cb683db..2b05f7c 100644 --- a/experiments/agent-attention/README.md +++ b/experiments/agent-attention/README.md @@ -1,6 +1,6 @@ # Agent Attention experiment -Source-and-test migration of the Agent Attention V2 prototype into the private +Source-and-test migration of the Agent Attention V2 prototype into the public plugin playground. ## Included @@ -17,6 +17,11 @@ does not claim a release-qualified plugin payload, persistent background wake, or live Apple Reminders proof. The generated plugin manifests remain owned by `plugin.config.json`; do not hand-edit them to activate this experiment. +The source-only Stop adapter currently uses a user-provided Bun executable. +Before plugin activation, route that adapter through the template-owned Bun +runtime-custody launcher. The Python/EventKit owner remains a separate runtime +dependency; template Bun custody does not replace it. + ## Verify ```sh From 84db1e8097738d97279168105f5889739b71c093 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Mon, 10 Aug 2026 17:42:47 +1000 Subject: [PATCH 03/24] fix: harden Agent Attention gate recovery --- .../hooks/agent-attention-codex-stop.test.ts | 2 +- .../hooks/agent-attention-codex-stop.ts | 2 +- .../agent-attention/agent-attention.py | 157 ++++++++++++------ .../agent-attention/install-link-handler.sh | 9 + .../agent-attention/test_agent_attention.py | 92 +++++++++- experiments/agent-attention/skill/SKILL.md | 2 +- 6 files changed, 210 insertions(+), 54 deletions(-) diff --git a/experiments/agent-attention/hooks/agent-attention-codex-stop.test.ts b/experiments/agent-attention/hooks/agent-attention-codex-stop.test.ts index 6d96289..feec4b8 100644 --- a/experiments/agent-attention/hooks/agent-attention-codex-stop.test.ts +++ b/experiments/agent-attention/hooks/agent-attention-codex-stop.test.ts @@ -97,7 +97,7 @@ describe('Agent Attention Codex Stop guard', () => { }) }) - test('default adapter reads only temporary structured owner state', async () => { + test('default adapter requires python3 on PATH and reads only temporary structured owner state', async () => { const temporary = await mkdtemp(join(tmpdir(), 'agent-attention-hook-')) const previous = process.env.XDG_STATE_HOME process.env.XDG_STATE_HOME = temporary diff --git a/experiments/agent-attention/hooks/agent-attention-codex-stop.ts b/experiments/agent-attention/hooks/agent-attention-codex-stop.ts index 160972f..9ea900d 100644 --- a/experiments/agent-attention/hooks/agent-attention-codex-stop.ts +++ b/experiments/agent-attention/hooks/agent-attention-codex-stop.ts @@ -133,7 +133,7 @@ function createDefaultRuntime(): AgentAttentionStopRuntime { ) const process = Bun.spawn( ['python3', owner, 'check-stop', '--thread-id', threadId], - { stdout: 'pipe', stderr: 'pipe' }, + { stdout: 'pipe', stderr: 'pipe', timeout: 5000 }, ) const [stdout, stderr, exitCode] = await Promise.all([ new Response(process.stdout).text(), diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index d1c7227..b04cb45 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -55,14 +55,27 @@ def load_json(path: Path) -> Any: def write_json(path: Path, value: Any, *, exclusive: bool = False) -> bool: """Write private JSON atomically enough for single-host gate custody.""" path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) - flags = os.O_WRONLY | os.O_CREAT | (os.O_EXCL if exclusive else os.O_TRUNC) + if exclusive: + try: + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + except FileExistsError: + return False + with os.fdopen(descriptor, "w", encoding="utf-8") as handle: + json.dump(value, handle, sort_keys=True) + handle.write("\n") + return True + + temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") try: - descriptor = os.open(path, flags, 0o600) - except FileExistsError: - return False - with os.fdopen(descriptor, "w", encoding="utf-8") as handle: - json.dump(value, handle, sort_keys=True) - handle.write("\n") + descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(descriptor, "w", encoding="utf-8") as handle: + json.dump(value, handle, sort_keys=True) + handle.write("\n") + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + finally: + temporary.unlink(missing_ok=True) return True @@ -145,19 +158,6 @@ def validate_thread_id(value: str) -> str: raise ContractError("thread ID must be one UUID") from error -def gate_notes(recommendation: str, approval_meaning: str) -> tuple[str, str]: - """Render concise recommendation-first notes and their required contract line.""" - required_line = f"Approval meaning: {approval_meaning}" - lines = [ - f"Recommended: {recommendation}", - "", - required_line, - "Tick = approve only this action.", - "Discuss or disagree: open Codex.", - ] - return "\n".join(lines), required_line - - def router_notes(intent: dict[str, Any]) -> tuple[str, str]: """Render one calm recommendation-first approval contract.""" required_line = f"Approval meaning: {intent['approval_meaning']}" @@ -273,18 +273,19 @@ def update_request_state( state = load_json(path) if state.get("reminder_id") != mapping["reminder_id"]: return - write_json( - path, - { - **state, - "status": status, - "updated_at": datetime.now(timezone.utc).isoformat(), - **values, - }, - ) + updated = { + **state, + "status": status, + "updated_at": datetime.now(timezone.utc).isoformat(), + **values, + } + for field, value in values.items(): + if value is None: + updated.pop(field, None) + write_json(path, updated) -def submit_approval(args: argparse.Namespace) -> dict[str, Any]: +def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: """Validate, deduplicate, and optionally create one native approval gate.""" state_dir: Path = args.state_dir intent, reasons = validate_approval_intent(approval_intent(args)) @@ -364,13 +365,13 @@ def submit_approval(args: argparse.Namespace) -> dict[str, Any]: thread_id=thread_id, repair="inspect exact request state before retry; no second gate was created", ) + args._agent_attention_request_lock_path = request_lock_path if path.exists(): existing_result = completed_or_active_request_result( load_json(path), request_identifier, thread_id ) if existing_result: - request_lock_path.unlink() return existing_result request_claim_path = state_dir / "request-claims" / f"{request_identifier}.json" @@ -521,7 +522,6 @@ def fail_created_verification(field: str) -> None: "updated_at": datetime.now(timezone.utc).isoformat(), }, ) - request_lock_path.unlink() return base_result( "gated", changed=True, @@ -533,6 +533,16 @@ def fail_created_verification(field: str) -> None: ) +def submit_approval(args: argparse.Namespace) -> dict[str, Any]: + """Release the owned per-thread request lock on every terminal path.""" + try: + return _submit_approval(args) + finally: + lock_path = getattr(args, "_agent_attention_request_lock_path", None) + if isinstance(lock_path, Path): + lock_path.unlink(missing_ok=True) + + def read_inventory( config: dict[str, Any], *, timeout_seconds: float | None = None ) -> list[dict[str, Any]]: @@ -572,6 +582,19 @@ def validate_event_id(value: str) -> str: return value +def validate_reminder_id(value: str) -> str: + """Reject path syntax while preserving one opaque stable reminder ID.""" + if ( + not value + or value in {".", ".."} + or "/" in value + or "\\" in value + or "\x00" in value + ): + raise ContractError("reminder ID must be one opaque path segment") + return value + + def validate_event_binding( value: Any, identifier: str, *, document_name: str ) -> dict[str, Any]: @@ -636,6 +659,7 @@ def contains_outcome_notes(current_notes: str, addition: str) -> bool: def read_gate_mapping(state_dir: Path, reminder_id: str) -> dict[str, Any]: """Load only the mapping owned by one exact stable reminder ID.""" + reminder_id = validate_reminder_id(reminder_id) path = state_dir / "gates" / f"{reminder_id}.json" if not path.exists(): raise ContractError("no gate mapping exists for the exact stable reminder ID") @@ -872,18 +896,51 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: if receipt_path.exists(): continue reminder = items_by_id.get(mapping.get("reminder_id")) + repair: str | None = None if not reminder: - raise ContractError("configured stable reminder ID did not resolve") - if reminder.get("listID") != config["list"]["id"]: - raise ContractError("reminder resolved outside the configured list") - if reminder.get("title") != mapping.get("expected_title"): - raise ContractError("reminder title changed; refusing semantic inference") - if mapping.get("required_notes_line") not in (reminder.get("notes") or "").splitlines(): - raise ContractError("approval meaning is absent from reminder notes") + repair = "configured stable reminder ID did not resolve" + elif reminder.get("listID") != config["list"]["id"]: + repair = "reminder resolved outside the configured list" + elif reminder.get("title") != mapping.get("expected_title"): + repair = "reminder title changed; refusing semantic inference" + elif mapping.get("required_notes_line") not in (reminder.get("notes") or "").splitlines(): + repair = "approval meaning is absent from reminder notes" + if repair: + repair = f"{repair}; reminder ID: {mapping.get('reminder_id')}" + write_json( + mapping_path, + { + **mapping, + "status": "repair", + "repair": repair, + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + update_request_state(state_dir, mapping, "repair", repair=repair) + continue + if mapping.get("status") == "repair": + mapping = { + field: value + for field, value in mapping.items() + if field not in {"status", "repair", "updated_at"} + } + write_json(mapping_path, mapping) + update_request_state(state_dir, mapping, "gated", repair=None) if not reminder.get("isCompleted"): continue if not reminder.get("completionDate"): - raise ContractError("completed reminder lacks a completion timestamp") + repair = f"completed reminder lacks a completion timestamp; reminder ID: {mapping['reminder_id']}" + write_json( + mapping_path, + { + **mapping, + "status": "repair", + "repair": repair, + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + update_request_state(state_dir, mapping, "repair", repair=repair) + continue claim_path = state_dir / "claims" / f"{identifier}.json" if claim_path.exists(): @@ -1001,11 +1058,7 @@ def record_delivery(args: argparse.Namespace) -> dict[str, Any]: } if not write_json(receipt_path, receipt, exclusive=True): return base_result("already_delivered", changed=False, event_id=identifier) - log_path = state_dir / "audit.jsonl" - log_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) - with log_path.open("a", encoding="utf-8") as handle: - handle.write(json.dumps(receipt, sort_keys=True) + "\n") - os.chmod(log_path, 0o600) + append_audit(state_dir / "audit.jsonl", receipt) update_request_state( state_dir, claim, @@ -1078,7 +1131,15 @@ def check_stop(args: argparse.Namespace) -> dict[str, Any]: def doctor(args: argparse.Namespace) -> dict[str, Any]: """Report readiness without reading private message or reminder content.""" state_dir: Path = args.state_dir - reminders = run_json(["remindctl", "doctor", "--for-agent", "--json"]) + reminders = run_json( + ["remindctl", "doctor", "--for-agent", "--json"], timeout_seconds=30 + ) + if not isinstance(reminders, dict): + raise ContractError("remindctl doctor must return a JSON object") + authorization = reminders.get("authorization") + authorized = bool( + isinstance(authorization, dict) and authorization.get("authorized") is True + ) config_path = state_dir / "config.json" config_status: dict[str, Any] = {"configured": False} if config_path.exists(): @@ -1098,11 +1159,11 @@ def doctor(args: argparse.Namespace) -> dict[str, Any]: ] handler["installed"] = "agent-attention" in schemes - ready = bool(reminders.get("authorization", {}).get("authorized")) and config_status["configured"] and handler["installed"] + ready = authorized and config_status["configured"] and handler["installed"] return base_result( "ready" if ready else "repair_needed", changed=False, - reminders={"authorized": reminders.get("authorization", {}).get("authorized", False)}, + reminders={"authorized": authorized}, config=config_status, link_handler=handler, next_safe_action=( diff --git a/experiments/agent-attention/runtime/agent-attention/install-link-handler.sh b/experiments/agent-attention/runtime/agent-attention/install-link-handler.sh index edac428..295bc07 100755 --- a/experiments/agent-attention/runtime/agent-attention/install-link-handler.sh +++ b/experiments/agent-attention/runtime/agent-attention/install-link-handler.sh @@ -8,6 +8,15 @@ contents_dir="$app_dir/Contents" macos_dir="$contents_dir/MacOS" register_bin="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister" +if ! command -v swiftc >/dev/null 2>&1; then + printf '{"status":"error","repair":"install Xcode Command Line Tools to provide swiftc"}\n' >&2 + exit 1 +fi +if [[ ! -x "$register_bin" ]]; then + printf '{"status":"error","repair":"lsregister is not available at the expected LaunchServices path"}\n' >&2 + exit 1 +fi + mkdir -p "$macos_dir" swiftc -parse-as-library "$source_dir/main.swift" -o "$macos_dir/AgentAttentionLink" cp "$source_dir/Info.plist" "$contents_dir/Info.plist" diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 83f0796..b2668d8 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -3,6 +3,7 @@ from __future__ import annotations +import importlib.util import json import os import subprocess @@ -11,11 +12,16 @@ import time from datetime import datetime, timezone import unittest +from unittest import mock from pathlib import Path from typing import Any 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) +RUNTIME_SPEC.loader.exec_module(AGENT_ATTENTION) LIST_ID = "32C46BA9-FE7A-4758-AA9E-4C4A249A5DF6" REMINDER_ID = "11111111-1111-4111-8111-111111111111" OTHER_REMINDER_ID = "22222222-2222-4222-8222-222222222222" @@ -151,8 +157,11 @@ def _write_fake_remindctl(self) -> None: print("unknown result after write", file=sys.stderr) raise SystemExit(5) print(json.dumps(matches[0])) +elif args[0] == "doctor": + print(os.environ.get("FAKE_REMINDERS_DOCTOR_JSON", '{"authorization":{"authorized":true}}')) else: - print(json.dumps({"authorization": {"authorized": True}})) + print("unsupported remindctl subcommand: " + args[0], file=sys.stderr) + raise SystemExit(2) """, encoding="utf-8", ) @@ -302,7 +311,28 @@ def test_outcome_preview_is_read_only_and_exact(self) -> None: ] ], ) - self.assertNotIn("Outcome:", self.target["notes"]) + inventory = json.loads(self.inventory_path.read_text()) + target = next(item for item in inventory if item["id"] == REMINDER_ID) + self.assertNotIn("Outcome:", target["notes"]) + + def test_outcome_rejects_path_like_reminder_id_before_mapping_lookup(self) -> None: + malicious_id = f"../requests/{THREAD_ID}" + request_dir = self.state_dir / "requests" + request_dir.mkdir() + (request_dir / f"{THREAD_ID}.json").write_text( + json.dumps({**self.mapping, "reminder_id": malicious_id}), encoding="utf-8" + ) + completed = self.run_cli( + "record-outcome", + "--reminder-id", + malicious_id, + "--outcome", + "Should not resolve outside gate custody.", + "--finished-at", + FINISHED_AT, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("reminder ID must be one opaque path segment", completed.stderr) def test_outcome_execute_updates_only_exact_completed_gate_once(self) -> None: arguments = ( @@ -597,6 +627,17 @@ def test_existing_submit_claim_suppresses_a_concurrent_gate_and_alert(self) -> N self.assertFalse(result["changed"]) self.assertEqual(self.calls(), []) + def test_failed_submit_releases_the_per_thread_request_lock(self) -> None: + completed = self.run_cli( + *self.submit_arguments(), + "--execute", + env_update={"FAKE_REMINDERS_NEW_ID": ""}, + ) + self.assertEqual(completed.returncode, 1) + self.assertFalse( + (self.state_dir / "request-locks" / f"{THREAD_ID}.json").exists() + ) + def test_stop_check_continues_declared_or_delivered_state_without_reading_prose(self) -> None: request_dir = self.state_dir / "requests" request_dir.mkdir() @@ -671,9 +712,54 @@ def test_watch_bounds_a_hung_remindctl_call(self) -> None: env_update={"FAKE_REMINDERS_HANG_SHOW_SECONDS": "2"}, ) self.assertEqual(completed.returncode, 1) - self.assertLess(time.monotonic() - started, 1) + self.assertLess(time.monotonic() - started, 1.9) self.assertIn("bounded execution window", completed.stderr) + def test_poll_records_repair_for_one_broken_gate_and_delivers_the_next(self) -> None: + for path in (self.state_dir / "receipts").glob("*.json"): + path.unlink() + later_meaning = "Approve the later bounded gate only." + later_mapping = { + **self.mapping, + "reminder_id": OTHER_REMINDER_ID, + "expected_title": "[APPROVE] Later bounded gate", + "required_notes_line": f"Approval meaning: {later_meaning}", + "approval_meaning": later_meaning, + } + (self.state_dir / "gates" / f"{OTHER_REMINDER_ID}.json").write_text( + json.dumps(later_mapping), encoding="utf-8" + ) + self.other.update( + { + "title": later_mapping["expected_title"], + "notes": later_mapping["required_notes_line"], + "isCompleted": True, + "completionDate": "2026-08-10T05:50:00Z", + } + ) + self.inventory_path.write_text(json.dumps([self.other]), encoding="utf-8") + + result = self.result(self.run_cli("poll")) + self.assertEqual(result["status"], "deliver") + self.assertIn(later_meaning, result["prompt"]) + broken = json.loads( + (self.state_dir / "gates" / f"{REMINDER_ID}.json").read_text() + ) + self.assertEqual(broken["status"], "repair") + self.assertIn(REMINDER_ID, broken["repair"]) + + def test_doctor_bounds_remindctl_and_rejects_non_object_json(self) -> None: + run_json = mock.Mock(return_value=[]) + with mock.patch.object(AGENT_ATTENTION, "run_json", run_json): + with self.assertRaises(AGENT_ATTENTION.ContractError): + AGENT_ATTENTION.doctor( + AGENT_ATTENTION.argparse.Namespace(state_dir=self.state_dir) + ) + run_json.assert_called_once_with( + ["remindctl", "doctor", "--for-agent", "--json"], + timeout_seconds=30, + ) + def test_record_delivery_rejects_invalid_event_ids_and_non_boolean_proof(self) -> None: for invalid_id in ("../requests/forged", "A" * 64, "0" * 63): with self.subTest(event_id=invalid_id): diff --git a/experiments/agent-attention/skill/SKILL.md b/experiments/agent-attention/skill/SKILL.md index 3f2a5d6..6e7b47a 100644 --- a/experiments/agent-attention/skill/SKILL.md +++ b/experiments/agent-attention/skill/SKILL.md @@ -25,7 +25,7 @@ claims, and outcome receipts. 4. If rejected, follow the returned repair or keep the decision in Codex. 5. When gated, leave the task paused. No response means no approval. 6. After exact-task delivery, apply only the stated approval meaning, run the - continuation, then use the owner’s outcome command. + continuation, then preview `record-outcome` and rerun it with `--execute`. Never infer approval from prose. Never create a second gate for the same request. Never delete or reopen the completed reminder. From 17415f9addbe65641c86f8dbb07e1c28706334f9 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Mon, 10 Aug 2026 22:06:26 +1000 Subject: [PATCH 04/24] test: preserve bootstrap proof after initialization --- scripts/ship-canary.test.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/scripts/ship-canary.test.ts b/scripts/ship-canary.test.ts index d7c56da..6079d64 100644 --- a/scripts/ship-canary.test.ts +++ b/scripts/ship-canary.test.ts @@ -471,13 +471,24 @@ test("candidate config cannot redirect trusted canary targets", () => { }) }) +function writeTrustedTemplateDriverConfig(driverRoot: string): void { + const configPath = join(driverRoot, "plugin.config.json") + const config = JSON.parse(readFileSync(configPath, "utf8")) as MutableBootstrapCandidate + config.template = true + config.repository = "https://github.com/myagentdojo/agent-plugin-template" + config.canary = { + owner: "myagentdojo", + actor: "myagentdojo", + publicRepository: "agent-plugin-template-canary-public", + privateRepository: "agent-plugin-template-canary-private", + } + writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`) +} + test("trusted template base admits exact first-consumer canary targets", () => { const driver = canaryFixture() const candidate = canaryFixture() - writeFileSync( - join(driver.temporaryRoot, "plugin.config.json"), - readFileSync(join(root, "plugin.config.json"), "utf8"), - ) + writeTrustedTemplateDriverConfig(driver.temporaryRoot) const result = runTrustedCanary( driver.temporaryRoot, @@ -550,10 +561,7 @@ test.each([ ])("trusted template bootstrap rejects %s", (_name, mutate, environment) => { const driver = canaryFixture() const candidate = canaryFixture() - writeFileSync( - join(driver.temporaryRoot, "plugin.config.json"), - readFileSync(join(root, "plugin.config.json"), "utf8"), - ) + writeTrustedTemplateDriverConfig(driver.temporaryRoot) const candidateConfigPath = join(candidate.temporaryRoot, "plugin.config.json") const candidateConfig = JSON.parse( readFileSync(candidateConfigPath, "utf8"), From dd78ce2cab35fd51f1b46227f02cb3209f3bec77 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 12:20:54 +1000 Subject: [PATCH 05/24] fix: keep startup failures inside structured boundary --- .../runtime/agent-attention/agent-attention.py | 2 +- .../runtime/agent-attention/test_agent_attention.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index b04cb45..bfae4c9 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -1241,8 +1241,8 @@ def parser() -> argparse.ArgumentParser: def main() -> int: """Dispatch one command and emit one JSON result.""" - args = parser().parse_args() try: + args = parser().parse_args() result = args.handler(args) except (ContractError, json.JSONDecodeError, OSError, subprocess.SubprocessError) as error: print(str(error), file=sys.stderr) diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index b2668d8..46028a1 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -283,6 +283,17 @@ def test_command_discovery_help_and_parser_stay_aligned(self) -> None: for term in ("yes_no", "paused", "Approve", "one alert"): self.assertIn(term, submit_help) + def test_parser_construction_errors_use_the_structured_error_boundary(self) -> None: + completed = self.run_cli( + "commands", env_update={"XDG_STATE_HOME": "relative-state"} + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("XDG_STATE_HOME must be an absolute path", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + result = json.loads(completed.stdout) + self.assertEqual(result["status"], "error") + self.assertEqual(result["error_category"], "contract_or_runtime") + def test_outcome_preview_is_read_only_and_exact(self) -> None: result = self.result( self.run_cli( From 9abd4c252dc71e19ff2bd99827cfbfedd3eb5e8d Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 13:20:30 +1000 Subject: [PATCH 06/24] fix: validate Agent Attention config shape --- .../runtime/agent-attention/agent-attention.py | 2 ++ .../runtime/agent-attention/test_agent_attention.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index bfae4c9..c86ba5e 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -126,6 +126,8 @@ def read_config(state_dir: Path) -> dict[str, Any]: "not configured; run configure with the exact Agent Attention list ID" ) config = load_json(path) + if not isinstance(config, dict): + raise ContractError("config must be a JSON object") if config.get("version") != 1: raise ContractError("unsupported config version") list_config = config.get("list") diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 46028a1..dd3d95c 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -294,6 +294,16 @@ def test_parser_construction_errors_use_the_structured_error_boundary(self) -> N self.assertEqual(result["status"], "error") self.assertEqual(result["error_category"], "contract_or_runtime") + def test_non_object_config_uses_the_structured_error_boundary(self) -> None: + (self.state_dir / "config.json").write_text("[]", encoding="utf-8") + completed = self.run_cli("doctor") + self.assertEqual(completed.returncode, 1) + self.assertIn("config must be a JSON object", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + result = json.loads(completed.stdout) + self.assertEqual(result["status"], "error") + self.assertEqual(result["error_category"], "contract_or_runtime") + def test_outcome_preview_is_read_only_and_exact(self) -> None: result = self.result( self.run_cli( From bb47b68e1c01635577c69fc4f405903694f08d36 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 13:31:12 +1000 Subject: [PATCH 07/24] Address PR review feedback (#1) - validate created reminder IDs before mapping writes - preserve structured repair state for invalid IDs --- .../runtime/agent-attention/agent-attention.py | 18 ++++++++++++++++-- .../agent-attention/test_agent_attention.py | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index c86ba5e..986ca27 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -447,6 +447,19 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: }, ) raise ContractError("created reminder response lacks a stable ID; inspect before retry") + try: + reminder_id = validate_reminder_id(reminder_id) + except ContractError as error: + write_json( + path, + { + **declared, + "status": "repair", + "repair": f"created reminder returned an invalid stable ID: {error}", + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + raise created_inventory = read_inventory(config) created_matches = [item for item in created_inventory if item.get("id") == reminder_id] if len(created_matches) != 1: @@ -584,10 +597,11 @@ def validate_event_id(value: str) -> str: return value -def validate_reminder_id(value: str) -> str: +def validate_reminder_id(value: Any) -> str: """Reject path syntax while preserving one opaque stable reminder ID.""" if ( - not value + not isinstance(value, str) + or not value or value in {".", ".."} or "/" in value or "\\" in value diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index dd3d95c..ec8b39e 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -659,6 +659,24 @@ def test_failed_submit_releases_the_per_thread_request_lock(self) -> None: (self.state_dir / "request-locks" / f"{THREAD_ID}.json").exists() ) + def test_submit_rejects_path_like_created_reminder_id_before_mapping_write(self) -> None: + completed = self.run_cli( + *self.submit_arguments(), + "--execute", + env_update={"FAKE_REMINDERS_NEW_ID": "../unexpected"}, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("reminder ID must be one opaque path segment", completed.stderr) + self.assertFalse((self.state_dir / "unexpected.json").exists()) + self.assertEqual( + [path.name for path in (self.state_dir / "gates").glob("*.json")], + [f"{REMINDER_ID}.json"], + ) + request = json.loads( + (self.state_dir / "requests" / f"{THREAD_ID}.json").read_text() + ) + self.assertEqual(request["status"], "repair") + def test_stop_check_continues_declared_or_delivered_state_without_reading_prose(self) -> None: request_dir = self.state_dir / "requests" request_dir.mkdir() From d3d72a770f07209aabfc9a97d27d37ae76bf006f Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 13:41:12 +1000 Subject: [PATCH 08/24] Address PR review feedback (#1) - release outcome claims after pre-edit failures - validate request state shape before Stop field access --- .../agent-attention/agent-attention.py | 15 ++++++-- .../agent-attention/test_agent_attention.py | 36 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 986ca27..87c14ce 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -802,8 +802,13 @@ def record_outcome(args: argparse.Namespace) -> dict[str, Any]: if existing_claim.get("outcome_id") != identifier: raise ContractError("a different terminal outcome claim already exists for this gate") - before = read_exact_completed_reminder(args.reminder_id, config["list"]["id"]) - validate_outcome_target(before, mapping, config) + try: + before = read_exact_completed_reminder(args.reminder_id, config["list"]["id"]) + validate_outcome_target(before, mapping, config) + except (ContractError, json.JSONDecodeError, OSError, subprocess.SubprocessError): + if claim_created: + claim_path.unlink(missing_ok=True) + raise current_notes = before.get("notes") or "" updated_notes = append_outcome_notes(current_notes, addition) if contains_outcome_notes(current_notes, addition): @@ -1098,7 +1103,11 @@ def check_stop(args: argparse.Namespace) -> dict[str, Any]: hook_action="allow", ) state = load_json(path) - if state.get("thread_id") != thread_id or state.get("version") != 1: + if ( + not isinstance(state, dict) + or state.get("thread_id") != thread_id + or state.get("version") != 1 + ): return base_result( "repair_needed", changed=False, diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index ec8b39e..93f4108 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -508,6 +508,31 @@ def test_outcome_rejects_incomplete_gate(self) -> None: self.assertEqual(completed.returncode, 1) self.assertIn("Completed history", completed.stderr) + def test_outcome_pre_edit_failure_releases_owned_claim_for_retry(self) -> None: + arguments = ( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Retryable terminal result.", + "--finished-at", + FINISHED_AT, + "--execute", + ) + self.target["isCompleted"] = False + self.target.pop("completionDate") + self._write_inventory() + failed = self.run_cli(*arguments) + self.assertEqual(failed.returncode, 1) + self.assertEqual(list((self.state_dir / "outcome-claims").glob("*.json")), []) + + self.target["isCompleted"] = True + self.target["completionDate"] = "2026-08-10T05:40:00Z" + self._write_inventory() + retried = self.result(self.run_cli(*arguments)) + self.assertEqual(retried["status"], "recorded") + self.assertEqual(len([call for call in self.calls() if call[0] == "edit"]), 1) + def test_outcome_rejects_missing_delivery_receipt(self) -> None: for path in (self.state_dir / "receipts").glob("*.json"): path.unlink() @@ -703,6 +728,17 @@ def test_stop_check_continues_declared_or_delivered_state_without_reading_prose( self.assertEqual(delivered["hook_action"], "continue") self.assertIn("Run the exact continuation", delivered["reason"]) + def test_stop_check_rejects_non_object_request_state_without_traceback(self) -> None: + request_dir = self.state_dir / "requests" + request_dir.mkdir() + (request_dir / f"{THREAD_ID}.json").write_text("[]", encoding="utf-8") + completed = self.run_cli("check-stop", "--thread-id", THREAD_ID) + result = self.result(completed) + self.assertEqual(result["status"], "repair_needed") + self.assertEqual(result["hook_action"], "continue") + self.assertIn("owner state is malformed", result["reason"]) + self.assertNotIn("Traceback", completed.stderr) + def test_watch_detects_and_records_delivery_under_fifteen_seconds(self) -> None: self.target["isCompleted"] = False self.target.pop("completionDate") From d280e294c403c9a20748cee1fbea412021e985ee Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 14:07:34 +1000 Subject: [PATCH 09/24] Address PR review feedback (#1) Validate persisted JSON ingress before field access or identity derivation. Add focused malformed-shape regressions across configuration, request, gate, inventory, delivery, and outcome state. --- .../agent-attention/agent-attention.py | 111 ++++++++++++---- .../agent-attention/test_agent_attention.py | 123 ++++++++++++++++++ 2 files changed, 209 insertions(+), 25 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 87c14ce..780a295 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -52,6 +52,29 @@ def load_json(path: Path) -> Any: return json.load(handle) +def require_json_object(value: Any, *, document_name: str) -> dict[str, Any]: + """Require one JSON object before field access.""" + if not isinstance(value, dict): + raise ContractError(f"{document_name} must be a JSON object") + return value + + +def require_nonempty_text(value: Any, *, field_name: str) -> str: + """Require one nonempty string at a persisted contract boundary.""" + if not isinstance(value, str) or not value.strip(): + raise ContractError(f"{field_name} must be nonempty text") + return value + + +def require_json_object_array(value: Any, *, document_name: str) -> list[dict[str, Any]]: + """Require one JSON array containing only objects.""" + if not isinstance(value, list): + raise ContractError(f"{document_name} must be a JSON array") + if any(not isinstance(item, dict) for item in value): + raise ContractError(f"{document_name} entries must be JSON objects") + return value + + def write_json(path: Path, value: Any, *, exclusive: bool = False) -> bool: """Write private JSON atomically enough for single-host gate custody.""" path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) @@ -125,29 +148,31 @@ def read_config(state_dir: Path) -> dict[str, Any]: raise ContractError( "not configured; run configure with the exact Agent Attention list ID" ) - config = load_json(path) - if not isinstance(config, dict): - raise ContractError("config must be a JSON object") + config = require_json_object(load_json(path), document_name="config") if config.get("version") != 1: raise ContractError("unsupported config version") list_config = config.get("list") - if not isinstance(list_config, dict) or not list_config.get("id") or not list_config.get("name"): + if not isinstance(list_config, dict): raise ContractError("configured list ID and name are required") + require_nonempty_text(list_config.get("id"), field_name="configured list ID") + require_nonempty_text(list_config.get("name"), field_name="configured list name") return config def configure(args: argparse.Namespace) -> dict[str, Any]: """Persist one explicit Apple Reminders list binding.""" state_dir: Path = args.state_dir + list_id = require_nonempty_text(args.list_id, field_name="list ID") + list_name = require_nonempty_text(args.list_name, field_name="list name") config = { "version": 1, - "list": {"id": args.list_id, "name": args.list_name}, + "list": {"id": list_id, "name": list_name}, } write_json(state_dir / "config.json", config) return base_result( "configured", changed=True, - list={"id": args.list_id, "name": args.list_name}, + list={"id": list_id, "name": list_name}, next_safe_action="run doctor", ) @@ -243,9 +268,10 @@ def request_path(state_dir: Path, thread_id: str) -> Path: def completed_or_active_request_result( - existing: dict[str, Any], request_identifier: str, thread_id: str + existing: Any, request_identifier: str, thread_id: str ) -> dict[str, Any] | None: """Return an idempotent result or reject a distinct active gate.""" + existing = require_json_object(existing, document_name="request state") status = existing.get("status") if status not in {"gated", "delivered", "completed"}: return None @@ -272,7 +298,7 @@ def update_request_state( path = request_path(state_dir, mapping["thread_id"]) if not path.exists(): return - state = load_json(path) + state = require_json_object(load_json(path), document_name="request state") if state.get("reminder_id") != mapping["reminder_id"]: return updated = { @@ -574,9 +600,7 @@ def read_inventory( ], timeout_seconds=timeout_seconds, ) - if not isinstance(inventory, list): - raise ContractError("remindctl inventory must be a JSON array") - return inventory + return require_json_object_array(inventory, document_name="remindctl inventory") def event_id(mapping: dict[str, Any]) -> str: @@ -625,6 +649,32 @@ def validate_event_binding( return value +def validate_gate_mapping( + value: Any, *, expected_reminder_id: str | None = None +) -> dict[str, Any]: + """Require one complete gate mapping before deriving identity.""" + mapping = require_json_object(value, document_name="gate mapping") + if mapping.get("version") != 1: + raise ContractError("unsupported gate mapping version") + reminder_id = validate_reminder_id(mapping.get("reminder_id")) + if expected_reminder_id is not None and reminder_id != expected_reminder_id: + raise ContractError("gate mapping stable reminder ID does not match") + for field in ( + "approval_meaning", + "expected_title", + "required_notes_line", + "thread_id", + ): + require_nonempty_text(mapping.get(field), field_name=f"gate mapping {field}") + validate_thread_id(mapping["thread_id"]) + list_config = mapping.get("list") + if not isinstance(list_config, dict): + raise ContractError("gate mapping list must be a JSON object") + require_nonempty_text(list_config.get("id"), field_name="gate mapping list ID") + require_nonempty_text(list_config.get("name"), field_name="gate mapping list name") + return mapping + + def outcome_id(mapping: dict[str, Any], outcome: str, finished_at: str) -> str: """Bind one terminal outcome receipt to its delivered approval event.""" payload = { @@ -679,19 +729,14 @@ def read_gate_mapping(state_dir: Path, reminder_id: str) -> dict[str, Any]: path = state_dir / "gates" / f"{reminder_id}.json" if not path.exists(): raise ContractError("no gate mapping exists for the exact stable reminder ID") - mapping = load_json(path) - if mapping.get("reminder_id") != reminder_id: - raise ContractError("gate mapping stable reminder ID does not match") - for field in ("approval_meaning", "expected_title", "required_notes_line", "thread_id"): - if not mapping.get(field): - raise ContractError(f"gate mapping lacks required field: {field}") - return mapping + return validate_gate_mapping(load_json(path), expected_reminder_id=reminder_id) def validate_delivery_receipt( - receipt: dict[str, Any], mapping: dict[str, Any], identifier: str + receipt: Any, mapping: dict[str, Any], identifier: str ) -> None: """Require one receipt bound to the exact delivered approval contract.""" + receipt = require_json_object(receipt, document_name="delivery receipt") expected = { "approval_meaning": mapping["approval_meaning"], "event_id": identifier, @@ -718,8 +763,9 @@ def read_exact_completed_reminder(reminder_id: str, list_id: str) -> dict[str, A "--no-input", ] ) - if not isinstance(inventory, list): - raise ContractError("completed reminder inventory must be a JSON array") + inventory = require_json_object_array( + inventory, document_name="completed reminder inventory" + ) matches = [item for item in inventory if item.get("id") == reminder_id] if len(matches) != 1: raise ContractError("exact stable reminder ID did not resolve once in Completed history") @@ -758,7 +804,9 @@ def record_outcome(args: argparse.Namespace) -> dict[str, Any]: identifier = outcome_id(mapping, outcome, finished_at) receipt_path = state_dir / "outcomes" / f"{delivery_id}.json" if receipt_path.exists(): - receipt = load_json(receipt_path) + receipt = require_json_object( + load_json(receipt_path), document_name="outcome receipt" + ) if receipt.get("outcome_id") != identifier: raise ContractError("a different terminal outcome is already recorded for this gate") update_request_state( @@ -798,7 +846,9 @@ def record_outcome(args: argparse.Namespace) -> dict[str, Any]: } claim_created = write_json(claim_path, claim, exclusive=True) if not claim_created: - existing_claim = load_json(claim_path) + existing_claim = require_json_object( + load_json(claim_path), document_name="outcome claim" + ) if existing_claim.get("outcome_id") != identifier: raise ContractError("a different terminal outcome claim already exists for this gate") @@ -911,7 +961,9 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: items_by_id = {item.get("id"): item for item in inventory} for mapping_path in mapping_paths: - mapping = load_json(mapping_path) + mapping = validate_gate_mapping( + load_json(mapping_path), expected_reminder_id=mapping_path.stem + ) identifier = event_id(mapping) receipt_path = state_dir / "receipts" / f"{identifier}.json" if receipt_path.exists(): @@ -1125,7 +1177,16 @@ def check_stop(args: argparse.Namespace) -> dict[str, Any]: reason="Agent Attention blocker was declared but has no gate or repair result. Finish submit or record an actionable repair.", ) if status == "delivered": - continuation = state.get("intent", {}).get("continuation") + intent = state.get("intent") + if not isinstance(intent, dict) or not isinstance(intent.get("continuation"), str): + return base_result( + "repair_needed", + changed=False, + thread_id=thread_id, + hook_action="continue", + reason="Agent Attention owner state is malformed. Repair the exact request state before stopping.", + ) + continuation = intent["continuation"] return base_result( "resume_needed", changed=False, diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 93f4108..069c86d 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -304,6 +304,44 @@ def test_non_object_config_uses_the_structured_error_boundary(self) -> None: self.assertEqual(result["status"], "error") self.assertEqual(result["error_category"], "contract_or_runtime") + def test_configure_rejects_empty_binding_before_overwriting_config(self) -> None: + config_path = self.state_dir / "config.json" + before = config_path.read_text() + completed = self.run_cli( + "configure", "--list-id", "", "--list-name", "Agent Attention" + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("list ID must be nonempty text", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(config_path.read_text(), before) + + def test_poll_rejects_non_object_gate_mapping_before_deriving_event_id(self) -> None: + (self.state_dir / "gates" / f"{REMINDER_ID}.json").write_text( + "[]", encoding="utf-8" + ) + completed = self.run_cli("poll") + self.assertEqual(completed.returncode, 1) + self.assertIn("gate mapping must be a JSON object", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + + def test_poll_rejects_incomplete_gate_mapping_before_deriving_event_id(self) -> None: + mapping = {**self.mapping} + mapping.pop("approval_meaning") + (self.state_dir / "gates" / f"{REMINDER_ID}.json").write_text( + json.dumps(mapping), encoding="utf-8" + ) + completed = self.run_cli("poll") + self.assertEqual(completed.returncode, 1) + self.assertIn("gate mapping approval_meaning must be nonempty text", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + + def test_poll_rejects_non_object_inventory_entry(self) -> None: + self.inventory_path.write_text(json.dumps([self.target, []]), encoding="utf-8") + completed = self.run_cli("poll") + self.assertEqual(completed.returncode, 1) + self.assertIn("inventory entries must be JSON objects", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + def test_outcome_preview_is_read_only_and_exact(self) -> None: result = self.result( self.run_cli( @@ -336,6 +374,60 @@ def test_outcome_preview_is_read_only_and_exact(self) -> None: target = next(item for item in inventory if item["id"] == REMINDER_ID) self.assertNotIn("Outcome:", target["notes"]) + def test_outcome_rejects_non_object_delivery_receipt(self) -> None: + receipt_path = next((self.state_dir / "receipts").glob("*.json")) + receipt_path.write_text("[]", encoding="utf-8") + completed = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Should fail.", + "--finished-at", + FINISHED_AT, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("delivery receipt must be a JSON object", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(self.calls(), []) + + def test_outcome_rejects_non_object_outcome_receipt(self) -> None: + outcomes_dir = self.state_dir / "outcomes" + outcomes_dir.mkdir() + (outcomes_dir / f"{self._event_id()}.json").write_text("[]", encoding="utf-8") + completed = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Should fail.", + "--finished-at", + FINISHED_AT, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("outcome receipt must be a JSON object", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(self.calls(), []) + + def test_outcome_rejects_non_object_outcome_claim(self) -> None: + claim_dir = self.state_dir / "outcome-claims" + claim_dir.mkdir() + (claim_dir / f"{self._event_id()}.json").write_text("[]", encoding="utf-8") + completed = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Should fail.", + "--finished-at", + FINISHED_AT, + "--execute", + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("outcome claim must be a JSON object", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(self.calls(), []) + def test_outcome_rejects_path_like_reminder_id_before_mapping_lookup(self) -> None: malicious_id = f"../requests/{THREAD_ID}" request_dir = self.state_dir / "requests" @@ -702,6 +794,16 @@ def test_submit_rejects_path_like_created_reminder_id_before_mapping_write(self) ) self.assertEqual(request["status"], "repair") + def test_submit_rejects_non_object_request_state_without_traceback(self) -> None: + request_dir = self.state_dir / "requests" + request_dir.mkdir() + (request_dir / f"{THREAD_ID}.json").write_text("[]", encoding="utf-8") + completed = self.run_cli(*self.submit_arguments()) + self.assertEqual(completed.returncode, 1) + self.assertIn("request state must be a JSON object", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(self.calls(), []) + def test_stop_check_continues_declared_or_delivered_state_without_reading_prose(self) -> None: request_dir = self.state_dir / "requests" request_dir.mkdir() @@ -739,6 +841,27 @@ def test_stop_check_rejects_non_object_request_state_without_traceback(self) -> self.assertIn("owner state is malformed", result["reason"]) self.assertNotIn("Traceback", completed.stderr) + def test_stop_check_rejects_non_object_delivered_intent_without_traceback(self) -> None: + request_dir = self.state_dir / "requests" + request_dir.mkdir() + (request_dir / f"{THREAD_ID}.json").write_text( + json.dumps( + { + "version": 1, + "thread_id": THREAD_ID, + "status": "delivered", + "intent": [], + } + ), + encoding="utf-8", + ) + completed = self.run_cli("check-stop", "--thread-id", THREAD_ID) + result = self.result(completed) + self.assertEqual(result["status"], "repair_needed") + self.assertEqual(result["hook_action"], "continue") + self.assertIn("owner state is malformed", result["reason"]) + self.assertNotIn("Traceback", completed.stderr) + def test_watch_detects_and_records_delivery_under_fifteen_seconds(self) -> None: self.target["isCompleted"] = False self.target.pop("completionDate") From 5755fd42a493de0eae51114af9faad64d93f94a9 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 14:20:20 +1000 Subject: [PATCH 10/24] Address PR review feedback (#1) Release the owned request claim when reminder creation cannot start. Preserve repair state and prove a later retry creates exactly one gate. --- .../runtime/agent-attention/agent-attention.py | 12 ++++++++++++ .../agent-attention/test_agent_attention.py | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 780a295..4494031 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -450,6 +450,18 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: "--no-input", ] ) + except OSError as error: + request_claim_path.unlink(missing_ok=True) + write_json( + path, + { + **declared, + "status": "repair", + "repair": f"gate creation did not start; repair remindctl before retry: {error}", + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + raise except ContractError as error: write_json( path, diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 069c86d..57bbee1 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -776,6 +776,23 @@ def test_failed_submit_releases_the_per_thread_request_lock(self) -> None: (self.state_dir / "request-locks" / f"{THREAD_ID}.json").exists() ) + def test_submit_releases_owned_claim_when_remindctl_never_starts(self) -> None: + missing_bin = self.root / "missing-bin" + missing_bin.mkdir() + arguments = (*self.submit_arguments(), "--execute") + failed = self.run_cli(*arguments, env_update={"PATH": str(missing_bin)}) + self.assertEqual(failed.returncode, 1) + self.assertIn("No such file or directory", failed.stderr) + self.assertEqual(list((self.state_dir / "request-claims").glob("*.json")), []) + request = json.loads( + (self.state_dir / "requests" / f"{THREAD_ID}.json").read_text() + ) + self.assertEqual(request["status"], "repair") + + retried = self.result(self.run_cli(*arguments)) + self.assertEqual(retried["status"], "gated") + self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_rejects_path_like_created_reminder_id_before_mapping_write(self) -> None: completed = self.run_cli( *self.submit_arguments(), From 9b53867e2c8d40cc3d95824437786d8b72b3d945 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 14:37:06 +1000 Subject: [PATCH 11/24] Address PR review feedback (#1) Release the owned outcome claim when reminder editing cannot start. Convert malformed link-handler plists and nested URL shapes into structured contract errors with retry regressions. --- .../agent-attention/agent-attention.py | 53 ++++++++++++------ .../agent-attention/test_agent_attention.py | 56 +++++++++++++++++++ 2 files changed, 91 insertions(+), 18 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 4494031..77bec6c 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -908,17 +908,21 @@ def record_outcome(args: argparse.Namespace) -> dict[str, Any]: repair="inspect the exact reminder before releasing this outcome claim", ) - run_json( - [ - "remindctl", - "edit", - args.reminder_id, - "--notes", - updated_notes, - "--json", - "--no-input", - ] - ) + try: + run_json( + [ + "remindctl", + "edit", + args.reminder_id, + "--notes", + updated_notes, + "--json", + "--no-input", + ] + ) + except OSError: + claim_path.unlink(missing_ok=True) + raise after = read_exact_completed_reminder(args.reminder_id, config["list"]["id"]) validate_outcome_target(after, mapping, config) if after.get("notes") != updated_notes: @@ -1248,13 +1252,26 @@ def doctor(args: argparse.Namespace) -> dict[str, Any]: info_path = handler_path / "Contents" / "Info.plist" handler = {"installed": False, "path": str(handler_path)} if info_path.exists(): - with info_path.open("rb") as handle: - info = plistlib.load(handle) - schemes = [ - scheme - for item in info.get("CFBundleURLTypes", []) - for scheme in item.get("CFBundleURLSchemes", []) - ] + try: + with info_path.open("rb") as handle: + info = plistlib.load(handle) + except (plistlib.InvalidFileException, TypeError, ValueError) as error: + raise ContractError("link handler Info.plist is malformed") from error + if not isinstance(info, dict): + raise ContractError("link handler Info.plist must contain a dictionary") + url_types = info.get("CFBundleURLTypes", []) + if not isinstance(url_types, list) or any( + not isinstance(item, dict) for item in url_types + ): + raise ContractError("link handler Info.plist URL types are malformed") + schemes: list[str] = [] + for item in url_types: + item_schemes = item.get("CFBundleURLSchemes", []) + if not isinstance(item_schemes, list) or any( + not isinstance(scheme, str) for scheme in item_schemes + ): + raise ContractError("link handler Info.plist URL schemes are malformed") + schemes.extend(item_schemes) handler["installed"] = "agent-attention" in schemes ready = authorized and config_status["configured"] and handler["installed"] diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 57bbee1..ad75b24 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -6,6 +6,7 @@ import importlib.util import json import os +import plistlib import subprocess import sys import tempfile @@ -127,6 +128,8 @@ def _write_fake_remindctl(self) -> None: inventory_path.write_text(json.dumps(inventory), encoding="utf-8") if args[1] == "completed": inventory = [item for item in inventory if item.get("isCompleted")] + if os.environ.get("FAKE_REMINDERS_REMOVE_AFTER_COMPLETED") == "1": + Path(sys.argv[0]).unlink() print(json.dumps(inventory)) elif args[0] == "add": reminder = { @@ -625,6 +628,36 @@ def test_outcome_pre_edit_failure_releases_owned_claim_for_retry(self) -> None: self.assertEqual(retried["status"], "recorded") self.assertEqual(len([call for call in self.calls() if call[0] == "edit"]), 1) + def test_outcome_releases_owned_claim_when_edit_never_starts(self) -> None: + arguments = ( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Retry after edit launch failure.", + "--finished-at", + FINISHED_AT, + "--execute", + ) + python_bin = self.root / "python-bin" + python_bin.mkdir() + (python_bin / "python3").symlink_to(sys.executable) + failed = self.run_cli( + *arguments, + env_update={ + "FAKE_REMINDERS_REMOVE_AFTER_COMPLETED": "1", + "PATH": f"{self.bin_dir}:{python_bin}:/usr/bin", + }, + ) + self.assertEqual(failed.returncode, 1) + self.assertIn("No such file or directory", failed.stderr) + self.assertEqual(list((self.state_dir / "outcome-claims").glob("*.json")), []) + + self._write_fake_remindctl() + retried = self.result(self.run_cli(*arguments)) + self.assertEqual(retried["status"], "recorded") + self.assertEqual(len([call for call in self.calls() if call[0] == "edit"]), 1) + def test_outcome_rejects_missing_delivery_receipt(self) -> None: for path in (self.state_dir / "receipts").glob("*.json"): path.unlink() @@ -975,6 +1008,29 @@ def test_doctor_bounds_remindctl_and_rejects_non_object_json(self) -> None: timeout_seconds=30, ) + def test_doctor_rejects_malformed_link_handler_plist_without_traceback(self) -> None: + home = self.root / "home" + info_path = home / "Applications" / "Agent Attention Link.app" / "Contents" / "Info.plist" + info_path.parent.mkdir(parents=True) + info_path.write_text("truncated", encoding="utf-8") + completed = self.run_cli("doctor", env_update={"HOME": str(home)}) + self.assertEqual(completed.returncode, 1) + self.assertIn("Info.plist is malformed", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(json.loads(completed.stdout)["status"], "error") + + def test_doctor_rejects_malformed_link_handler_plist_shape(self) -> None: + home = self.root / "home" + info_path = home / "Applications" / "Agent Attention Link.app" / "Contents" / "Info.plist" + info_path.parent.mkdir(parents=True) + with info_path.open("wb") as handle: + plistlib.dump({"CFBundleURLTypes": [[]]}, handle) + completed = self.run_cli("doctor", env_update={"HOME": str(home)}) + self.assertEqual(completed.returncode, 1) + self.assertIn("Info.plist URL types are malformed", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(json.loads(completed.stdout)["status"], "error") + def test_record_delivery_rejects_invalid_event_ids_and_non_boolean_proof(self) -> None: for invalid_id in ("../requests/forged", "A" * 64, "0" * 63): with self.subTest(event_id=invalid_id): From 7b276dcf2951f0474ca6c889819e83d254eacb67 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 14:54:10 +1000 Subject: [PATCH 12/24] Address PR review feedback (#1) Validate timezone-aware completion timestamps before claiming delivery. Verify existing delivery receipts against the exact gate before suppressing an event. --- .../agent-attention/agent-attention.py | 32 +++++++++++---- .../agent-attention/test_agent_attention.py | 41 +++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 77bec6c..8bebfa1 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -708,13 +708,21 @@ def validate_outcome(value: str) -> str: def validate_finished_at(value: str) -> str: """Require an explicit timezone-aware terminal timestamp.""" + parse_timezone_aware_timestamp(value, field_name="finished-at") + return value + + +def parse_timezone_aware_timestamp(value: Any, *, field_name: str) -> datetime: + """Parse one ISO 8601 timestamp with an explicit timezone.""" + if not isinstance(value, str): + raise ContractError(f"{field_name} must be an ISO 8601 timestamp") try: parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError as error: - raise ContractError("finished-at must be an ISO 8601 timestamp") from error + raise ContractError(f"{field_name} must be an ISO 8601 timestamp") from error if parsed.tzinfo is None: - raise ContractError("finished-at must include a timezone") - return value + raise ContractError(f"{field_name} must include a timezone") + return parsed def append_outcome_notes(current_notes: str, addition: str) -> str: @@ -983,6 +991,7 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: identifier = event_id(mapping) receipt_path = state_dir / "receipts" / f"{identifier}.json" if receipt_path.exists(): + validate_delivery_receipt(load_json(receipt_path), mapping, identifier) continue reminder = items_by_id.get(mapping.get("reminder_id")) repair: str | None = None @@ -1017,8 +1026,13 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: update_request_state(state_dir, mapping, "gated", repair=None) if not reminder.get("isCompleted"): continue - if not reminder.get("completionDate"): - repair = f"completed reminder lacks a completion timestamp; reminder ID: {mapping['reminder_id']}" + completion_date = reminder.get("completionDate") + try: + parse_timezone_aware_timestamp( + completion_date, field_name="reminder completionDate" + ) + except ContractError as error: + repair = f"{error}; reminder ID: {mapping['reminder_id']}" write_json( mapping_path, { @@ -1044,7 +1058,7 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: "claimed_at": datetime.now(timezone.utc).isoformat(), "event_id": identifier, "reminder_id": mapping["reminder_id"], - "completion_date": reminder["completionDate"], + "completion_date": completion_date, "thread_id": mapping["thread_id"], "approval_meaning": mapping["approval_meaning"], } @@ -1054,7 +1068,7 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: "deliver", changed=True, event_id=identifier, - completion_date=reminder["completionDate"], + completion_date=completion_date, thread_id=mapping["thread_id"], prompt=( f"Agent Attention approval received. {mapping['approval_meaning']} " @@ -1089,8 +1103,8 @@ def watch(args: argparse.Namespace) -> dict[str, Any]: polls += 1 if result["status"] in {"deliver", "claimed"}: if result["status"] == "deliver": - completed = datetime.fromisoformat( - result["completion_date"].replace("Z", "+00:00") + completed = parse_timezone_aware_timestamp( + result["completion_date"], field_name="reminder completionDate" ) result["detection_latency_seconds"] = round( max(0.0, (datetime.now(timezone.utc) - completed).total_seconds()), 3 diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index ad75b24..fda05b3 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -963,6 +963,47 @@ def test_watch_bounds_a_hung_remindctl_call(self) -> None: self.assertLess(time.monotonic() - started, 1.9) self.assertIn("bounded execution window", completed.stderr) + def test_poll_repairs_invalid_completion_timestamp_before_claiming(self) -> None: + for path in (self.state_dir / "receipts").glob("*.json"): + path.unlink() + self.target["completionDate"] = "2026-08-10T05:40:00" + self._write_inventory() + completed = self.run_cli("poll") + result = self.result(completed) + self.assertEqual(result["status"], "waiting") + self.assertEqual(list((self.state_dir / "claims").glob("*.json")), []) + mapping = json.loads( + (self.state_dir / "gates" / f"{REMINDER_ID}.json").read_text() + ) + self.assertEqual(mapping["status"], "repair") + self.assertIn("completionDate must include a timezone", mapping["repair"]) + self.assertNotIn("Traceback", completed.stderr) + + def test_poll_rejects_malformed_or_mismatched_existing_receipt(self) -> None: + receipt_path = next((self.state_dir / "receipts").glob("*.json")) + for receipt, expected_error in ( + ("{", "Expecting property name"), + ( + json.dumps( + { + "event_id": self._event_id(), + "reminder_id": REMINDER_ID, + "thread_id": OTHER_REMINDER_ID, + "approval_meaning": APPROVAL_MEANING, + "delivered_at": "2026-08-10T05:41:00Z", + } + ), + "does not match gate field: thread_id", + ), + ): + with self.subTest(expected_error=expected_error): + receipt_path.write_text(receipt, encoding="utf-8") + completed = self.run_cli("poll") + self.assertEqual(completed.returncode, 1) + self.assertIn(expected_error, completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(list((self.state_dir / "claims").glob("*.json")), []) + def test_poll_records_repair_for_one_broken_gate_and_delivers_the_next(self) -> None: for path in (self.state_dir / "receipts").glob("*.json"): path.unlink() From 467fb29b34371455716bb7473f2b470f67ac580c Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 15:11:01 +1000 Subject: [PATCH 13/24] Address PR review feedback (#1) Reject non-finite watch interval and timeout values before deadline arithmetic or sleep. Add public CLI regressions for NaN and infinity. --- .../runtime/agent-attention/agent-attention.py | 13 +++++++++++-- .../agent-attention/test_agent_attention.py | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 8bebfa1..0cfbbcb 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -6,6 +6,7 @@ import argparse import hashlib import json +import math import os import plistlib import subprocess @@ -1082,9 +1083,17 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: def watch(args: argparse.Namespace) -> dict[str, Any]: """Wait in the foreground for one bounded completion detection window.""" - if args.interval_seconds <= 0 or args.interval_seconds > 15: + if ( + not math.isfinite(args.interval_seconds) + or args.interval_seconds <= 0 + or args.interval_seconds > 15 + ): raise ContractError("interval-seconds must be greater than zero and at most 15") - if args.timeout_seconds <= 0 or args.timeout_seconds > 3600: + if ( + not math.isfinite(args.timeout_seconds) + or args.timeout_seconds <= 0 + or args.timeout_seconds > 3600 + ): raise ContractError("timeout-seconds must be greater than zero and at most 3600") started = time.monotonic() deadline = started + args.timeout_seconds diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index fda05b3..7c0f48e 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -963,6 +963,23 @@ def test_watch_bounds_a_hung_remindctl_call(self) -> None: self.assertLess(time.monotonic() - started, 1.9) self.assertIn("bounded execution window", completed.stderr) + def test_watch_rejects_non_finite_durations_without_traceback(self) -> None: + for flag, value, expected_error in ( + ("--interval-seconds", "nan", "interval-seconds"), + ("--interval-seconds", "inf", "interval-seconds"), + ("--timeout-seconds", "nan", "timeout-seconds"), + ("--timeout-seconds", "inf", "timeout-seconds"), + ): + with self.subTest(flag=flag, value=value): + started = time.monotonic() + completed = self.run_cli("watch", flag, value) + self.assertEqual(completed.returncode, 1) + self.assertLess(time.monotonic() - started, 1) + self.assertIn(expected_error, completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(json.loads(completed.stdout)["status"], "error") + self.assertEqual(self.calls(), []) + def test_poll_repairs_invalid_completion_timestamp_before_claiming(self) -> None: for path in (self.state_dir / "receipts").glob("*.json"): path.unlink() From 1ac5d32d376797ab851aae9b568ee7a0c6710e1d Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 15:30:46 +1000 Subject: [PATCH 14/24] Address PR review feedback (#1) Use a crash-recoverable advisory lock for per-thread request admission while preserving permanent request claims. Require the plist-selected regular executable before reporting link-handler readiness. --- .../agent-attention/agent-attention.py | 89 +++++++++++++++++-- .../agent-attention/test_agent_attention.py | 59 +++++++++++- 2 files changed, 136 insertions(+), 12 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 0cfbbcb..3b16b72 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import fcntl import hashlib import json import math @@ -103,6 +104,55 @@ def write_json(path: Path, value: Any, *, exclusive: bool = False) -> bool: return True +def acquire_request_lock(path: Path, value: dict[str, Any]) -> int | None: + """Acquire one crash-recoverable process-owned request lock.""" + path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + flags = os.O_RDWR | os.O_CREAT + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + descriptor = os.open(path, flags, 0o600) + try: + fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + os.close(descriptor) + return None + try: + encoded = (json.dumps(value, sort_keys=True) + "\n").encode() + os.fchmod(descriptor, 0o600) + os.ftruncate(descriptor, 0) + os.lseek(descriptor, 0, os.SEEK_SET) + remaining = memoryview(encoded) + while remaining: + written = os.write(descriptor, remaining) + if written <= 0: + raise OSError("request lock metadata write made no progress") + remaining = remaining[written:] + os.fsync(descriptor) + except BaseException: + fcntl.flock(descriptor, fcntl.LOCK_UN) + os.close(descriptor) + raise + return descriptor + + +def release_request_lock(path: Path, descriptor: int) -> None: + """Remove only the path still backed by the held request lock.""" + try: + try: + path_stat = os.stat(path, follow_symlinks=False) + except FileNotFoundError: + path_stat = None + descriptor_stat = os.fstat(descriptor) + if path_stat and ( + path_stat.st_dev == descriptor_stat.st_dev + and path_stat.st_ino == descriptor_stat.st_ino + ): + path.unlink() + finally: + fcntl.flock(descriptor, fcntl.LOCK_UN) + os.close(descriptor) + + def append_audit(path: Path, value: Any) -> None: """Append one private JSON audit event.""" path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) @@ -378,15 +428,15 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: ) request_lock_path = state_dir / "request-locks" / f"{thread_id}.json" - if not write_json( + request_lock_descriptor = acquire_request_lock( request_lock_path, { "request_id": request_identifier, "thread_id": thread_id, "locked_at": datetime.now(timezone.utc).isoformat(), }, - exclusive=True, - ): + ) + if request_lock_descriptor is None: return base_result( "claimed", changed=False, @@ -394,7 +444,10 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: thread_id=thread_id, repair="inspect exact request state before retry; no second gate was created", ) - args._agent_attention_request_lock_path = request_lock_path + args._agent_attention_request_lock = ( + request_lock_path, + request_lock_descriptor, + ) if path.exists(): existing_result = completed_or_active_request_result( @@ -592,9 +645,14 @@ def submit_approval(args: argparse.Namespace) -> dict[str, Any]: try: return _submit_approval(args) finally: - lock_path = getattr(args, "_agent_attention_request_lock_path", None) - if isinstance(lock_path, Path): - lock_path.unlink(missing_ok=True) + request_lock = getattr(args, "_agent_attention_request_lock", None) + if ( + isinstance(request_lock, tuple) + and len(request_lock) == 2 + and isinstance(request_lock[0], Path) + and isinstance(request_lock[1], int) + ): + release_request_lock(request_lock[0], request_lock[1]) def read_inventory( @@ -1295,7 +1353,22 @@ def doctor(args: argparse.Namespace) -> dict[str, Any]: ): raise ContractError("link handler Info.plist URL schemes are malformed") schemes.extend(item_schemes) - handler["installed"] = "agent-attention" in schemes + executable_name = info.get("CFBundleExecutable") + if ( + not isinstance(executable_name, str) + or not executable_name + or executable_name in {".", ".."} + or "/" in executable_name + or "\\" in executable_name + ): + raise ContractError("link handler Info.plist executable is malformed") + executable_path = handler_path / "Contents" / "MacOS" / executable_name + executable_ready = ( + executable_path.is_file() + and not executable_path.is_symlink() + and os.access(executable_path, os.X_OK) + ) + handler["installed"] = "agent-attention" in schemes and executable_ready ready = authorized and config_status["configured"] and handler["installed"] return base_result( diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 7c0f48e..5a30e5e 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -3,6 +3,7 @@ from __future__ import annotations +import fcntl import importlib.util import json import os @@ -787,17 +788,33 @@ def test_submit_rejects_information_updates_and_non_blockers(self) -> None: def test_existing_submit_claim_suppresses_a_concurrent_gate_and_alert(self) -> None: claim_dir = self.state_dir / "request-locks" claim_dir.mkdir() - (claim_dir / f"{THREAD_ID}.json").write_text( + lock_path = claim_dir / f"{THREAD_ID}.json" + lock_path.write_text( json.dumps({"thread_id": THREAD_ID, "request_id": "winner"}), encoding="utf-8", ) - result = self.result( - self.run_cli(*self.submit_arguments(), "--execute") - ) + with lock_path.open("r+") as lock_handle: + fcntl.flock(lock_handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + result = self.result( + self.run_cli(*self.submit_arguments(), "--execute") + ) self.assertEqual(result["status"], "claimed") self.assertFalse(result["changed"]) self.assertEqual(self.calls(), []) + def test_submit_recovers_request_lock_abandoned_before_creation(self) -> None: + claim_dir = self.state_dir / "request-locks" + claim_dir.mkdir() + lock_path = claim_dir / f"{THREAD_ID}.json" + lock_path.write_text( + json.dumps({"thread_id": THREAD_ID, "request_id": "abandoned"}), + encoding="utf-8", + ) + result = self.result(self.run_cli(*self.submit_arguments(), "--execute")) + self.assertEqual(result["status"], "gated") + self.assertFalse(lock_path.exists()) + self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_failed_submit_releases_the_per_thread_request_lock(self) -> None: completed = self.run_cli( *self.submit_arguments(), @@ -1089,6 +1106,40 @@ def test_doctor_rejects_malformed_link_handler_plist_shape(self) -> None: self.assertNotIn("Traceback", completed.stderr) self.assertEqual(json.loads(completed.stdout)["status"], "error") + def test_doctor_requires_the_plist_selected_executable_for_readiness(self) -> None: + home = self.root / "home" + handler_path = home / "Applications" / "Agent Attention Link.app" + info_path = handler_path / "Contents" / "Info.plist" + info_path.parent.mkdir(parents=True) + with info_path.open("wb") as handle: + plistlib.dump( + { + "CFBundleExecutable": "AgentAttentionLink", + "CFBundleURLTypes": [ + {"CFBundleURLSchemes": ["agent-attention"]} + ], + }, + handle, + ) + missing = self.result(self.run_cli("doctor", env_update={"HOME": str(home)})) + self.assertEqual(missing["status"], "repair_needed") + self.assertFalse(missing["link_handler"]["installed"]) + + executable = handler_path / "Contents" / "MacOS" / "AgentAttentionLink" + executable.parent.mkdir() + executable.write_text("#!/bin/sh\n", encoding="utf-8") + executable.chmod(0o644) + non_executable = self.result( + self.run_cli("doctor", env_update={"HOME": str(home)}) + ) + self.assertEqual(non_executable["status"], "repair_needed") + self.assertFalse(non_executable["link_handler"]["installed"]) + + executable.chmod(0o755) + ready = self.result(self.run_cli("doctor", env_update={"HOME": str(home)})) + self.assertEqual(ready["status"], "ready") + self.assertTrue(ready["link_handler"]["installed"]) + def test_record_delivery_rejects_invalid_event_ids_and_non_boolean_proof(self) -> None: for invalid_id in ("../requests/forged", "A" * 64, "0" * 63): with self.subTest(event_id=invalid_id): From 17f2ef3080bd8927355ade36916b3bcd48f54851 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 15:43:47 +1000 Subject: [PATCH 15/24] Address PR review feedback (#1) Require literal reminder completion before delivery or outcome mutation. Validate receipt timestamps and reconcile matching request state from durable delivery receipts. --- .../agent-attention/agent-attention.py | 37 ++++++++-- .../agent-attention/test_agent_attention.py | 70 +++++++++++++++++++ 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 3b16b72..f89859d 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -813,7 +813,7 @@ def read_gate_mapping(state_dir: Path, reminder_id: str) -> dict[str, Any]: def validate_delivery_receipt( receipt: Any, mapping: dict[str, Any], identifier: str -) -> None: +) -> dict[str, Any]: """Require one receipt bound to the exact delivered approval contract.""" receipt = require_json_object(receipt, document_name="delivery receipt") expected = { @@ -825,8 +825,10 @@ def validate_delivery_receipt( for field, value in expected.items(): if receipt.get(field) != value: raise ContractError(f"delivery receipt does not match gate field: {field}") - if not receipt.get("delivered_at"): - raise ContractError("delivery receipt lacks delivered_at") + parse_timezone_aware_timestamp( + receipt.get("delivered_at"), field_name="delivery receipt delivered_at" + ) + return receipt def read_exact_completed_reminder(reminder_id: str, list_id: str) -> dict[str, Any]: @@ -863,7 +865,7 @@ def validate_outcome_target( raise ContractError("reminder title changed; refusing outcome update") if mapping.get("required_notes_line") not in (reminder.get("notes") or "").splitlines(): raise ContractError("approval meaning is absent from reminder notes") - if not reminder.get("isCompleted") or not reminder.get("completionDate"): + if reminder.get("isCompleted") is not True or not reminder.get("completionDate"): raise ContractError("outcome requires an already completed reminder") @@ -1050,7 +1052,16 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: identifier = event_id(mapping) receipt_path = state_dir / "receipts" / f"{identifier}.json" if receipt_path.exists(): - validate_delivery_receipt(load_json(receipt_path), mapping, identifier) + receipt = validate_delivery_receipt( + load_json(receipt_path), mapping, identifier + ) + update_request_state( + state_dir, + mapping, + "delivered", + event_id=identifier, + delivered_at=receipt["delivered_at"], + ) continue reminder = items_by_id.get(mapping.get("reminder_id")) repair: str | None = None @@ -1083,7 +1094,21 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: } write_json(mapping_path, mapping) update_request_state(state_dir, mapping, "gated", repair=None) - if not reminder.get("isCompleted"): + completion_state = reminder.get("isCompleted") + if completion_state is False: + continue + if completion_state is not True: + repair = f"reminder isCompleted must be a JSON boolean; reminder ID: {mapping['reminder_id']}" + write_json( + mapping_path, + { + **mapping, + "status": "repair", + "repair": repair, + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + update_request_state(state_dir, mapping, "repair", repair=repair) continue completion_date = reminder.get("completionDate") try: diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 5a30e5e..64760b2 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -1029,6 +1029,18 @@ def test_poll_rejects_malformed_or_mismatched_existing_receipt(self) -> None: ), "does not match gate field: thread_id", ), + ( + json.dumps( + { + "event_id": self._event_id(), + "reminder_id": REMINDER_ID, + "thread_id": THREAD_ID, + "approval_meaning": APPROVAL_MEANING, + "delivered_at": "not-a-timestamp", + } + ), + "delivery receipt delivered_at must be an ISO 8601 timestamp", + ), ): with self.subTest(expected_error=expected_error): receipt_path.write_text(receipt, encoding="utf-8") @@ -1038,6 +1050,64 @@ def test_poll_rejects_malformed_or_mismatched_existing_receipt(self) -> None: self.assertNotIn("Traceback", completed.stderr) self.assertEqual(list((self.state_dir / "claims").glob("*.json")), []) + def test_poll_repairs_non_boolean_completion_without_claiming(self) -> None: + for path in (self.state_dir / "receipts").glob("*.json"): + path.unlink() + for completion_state in ("false", 1, None): + with self.subTest(completion_state=completion_state): + self.target["isCompleted"] = completion_state + self._write_inventory() + completed = self.run_cli("poll") + result = self.result(completed) + self.assertEqual(result["status"], "waiting") + self.assertEqual(list((self.state_dir / "claims").glob("*.json")), []) + mapping = json.loads( + (self.state_dir / "gates" / f"{REMINDER_ID}.json").read_text() + ) + self.assertEqual(mapping["status"], "repair") + self.assertIn("isCompleted must be a JSON boolean", mapping["repair"]) + self.assertNotIn("Traceback", completed.stderr) + + def test_poll_reconciles_request_state_from_existing_delivery_receipt(self) -> None: + request_dir = self.state_dir / "requests" + request_dir.mkdir() + request_path = request_dir / f"{THREAD_ID}.json" + request_path.write_text( + json.dumps( + { + "version": 1, + "request_id": "existing-request", + "thread_id": THREAD_ID, + "reminder_id": REMINDER_ID, + "intent": {"continuation": "Resume exact work."}, + "status": "gated", + } + ), + encoding="utf-8", + ) + result = self.result(self.run_cli("poll")) + self.assertEqual(result["status"], "waiting") + request = json.loads(request_path.read_text()) + self.assertEqual(request["status"], "delivered") + self.assertEqual(request["event_id"], self._event_id()) + self.assertEqual(request["delivered_at"], "2026-08-10T05:41:00Z") + + def test_outcome_rejects_truthy_non_boolean_completion(self) -> None: + self.target["isCompleted"] = "false" + self._write_inventory() + completed = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Must not record.", + "--finished-at", + FINISHED_AT, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("already completed reminder", completed.stderr) + self.assertEqual([call[0] for call in self.calls()], ["show"]) + def test_poll_records_repair_for_one_broken_gate_and_delivers_the_next(self) -> None: for path in (self.state_dir / "receipts").glob("*.json"): path.unlink() From 345417d5a9a80e1322713f290f504e94fd02b247 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 15:57:48 +1000 Subject: [PATCH 16/24] Address PR review feedback (#1) Reject non-text reminder notes through the structured contract boundary and release owned outcome claims. Fully validate existing delivery receipts before reconciling owner state. --- .../agent-attention/agent-attention.py | 27 ++++++++-- .../agent-attention/test_agent_attention.py | 53 +++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index f89859d..cbfa571 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -825,12 +825,22 @@ def validate_delivery_receipt( for field, value in expected.items(): if receipt.get(field) != value: raise ContractError(f"delivery receipt does not match gate field: {field}") + validate_event_binding(receipt, identifier, document_name="delivery receipt") parse_timezone_aware_timestamp( receipt.get("delivered_at"), field_name="delivery receipt delivered_at" ) return receipt +def reminder_notes(value: Any) -> str: + """Normalize absent reminder notes and reject schema drift.""" + if value is None: + return "" + if not isinstance(value, str): + raise ContractError("reminder notes must be text") + return value + + def read_exact_completed_reminder(reminder_id: str, list_id: str) -> dict[str, Any]: """Resolve one stable ID inside the configured list's Completed view.""" inventory = run_json( @@ -863,7 +873,9 @@ def validate_outcome_target( raise ContractError("gate mapping list does not match configuration") if reminder.get("title") != mapping.get("expected_title"): raise ContractError("reminder title changed; refusing outcome update") - if mapping.get("required_notes_line") not in (reminder.get("notes") or "").splitlines(): + if mapping.get("required_notes_line") not in reminder_notes( + reminder.get("notes") + ).splitlines(): raise ContractError("approval meaning is absent from reminder notes") if reminder.get("isCompleted") is not True or not reminder.get("completionDate"): raise ContractError("outcome requires an already completed reminder") @@ -940,7 +952,7 @@ def record_outcome(args: argparse.Namespace) -> dict[str, Any]: if claim_created: claim_path.unlink(missing_ok=True) raise - current_notes = before.get("notes") or "" + current_notes = reminder_notes(before.get("notes")) updated_notes = append_outcome_notes(current_notes, addition) if contains_outcome_notes(current_notes, addition): receipt = { @@ -1071,7 +1083,9 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: repair = "reminder resolved outside the configured list" elif reminder.get("title") != mapping.get("expected_title"): repair = "reminder title changed; refusing semantic inference" - elif mapping.get("required_notes_line") not in (reminder.get("notes") or "").splitlines(): + elif mapping.get("required_notes_line") not in reminder_notes( + reminder.get("notes") + ).splitlines(): repair = "approval meaning is absent from reminder notes" if repair: repair = f"{repair}; reminder ID: {mapping.get('reminder_id')}" @@ -1222,8 +1236,11 @@ def record_delivery(args: argparse.Namespace) -> dict[str, Any]: claim_path = state_dir / "claims" / f"{identifier}.json" receipt_path = state_dir / "receipts" / f"{identifier}.json" if receipt_path.exists(): - receipt = validate_event_binding( - load_json(receipt_path), identifier, document_name="delivery receipt" + receipt_candidate = require_json_object( + load_json(receipt_path), document_name="delivery receipt" + ) + receipt = validate_delivery_receipt( + receipt_candidate, receipt_candidate, identifier ) update_request_state( state_dir, diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 64760b2..fef7d1c 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -1108,6 +1108,24 @@ def test_outcome_rejects_truthy_non_boolean_completion(self) -> None: self.assertIn("already completed reminder", completed.stderr) self.assertEqual([call[0] for call in self.calls()], ["show"]) + def test_outcome_rejects_non_text_notes_and_releases_owned_claim(self) -> None: + self.target["notes"] = {"unexpected": "shape"} + self._write_inventory() + completed = self.run_cli( + "record-outcome", + "--reminder-id", + REMINDER_ID, + "--outcome", + "Must not record.", + "--finished-at", + FINISHED_AT, + "--execute", + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("reminder notes must be text", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(list((self.state_dir / "outcome-claims").glob("*.json")), []) + def test_poll_records_repair_for_one_broken_gate_and_delivers_the_next(self) -> None: for path in (self.state_dir / "receipts").glob("*.json"): path.unlink() @@ -1250,6 +1268,41 @@ def test_record_delivery_rejects_invalid_event_ids_and_non_boolean_proof(self) - self.assertEqual(completed.returncode, 1) self.assertIn("does not confirm delivery", completed.stderr) + def test_record_delivery_rejects_incomplete_existing_receipt_before_reconciliation(self) -> None: + request_dir = self.state_dir / "requests" + request_dir.mkdir() + request_path = request_dir / f"{THREAD_ID}.json" + request_path.write_text( + json.dumps( + { + "version": 1, + "request_id": "existing-request", + "thread_id": THREAD_ID, + "reminder_id": REMINDER_ID, + "intent": {"continuation": "Resume exact work."}, + "status": "gated", + } + ), + encoding="utf-8", + ) + receipt_path = next((self.state_dir / "receipts").glob("*.json")) + receipt = json.loads(receipt_path.read_text()) + receipt.pop("delivered_at") + receipt_path.write_text(json.dumps(receipt), encoding="utf-8") + completed = self.run_cli( + "record-delivery", + "--event-id", + self._event_id(), + "--tool-result", + '{"delivered":true}', + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("delivery receipt delivered_at", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + request = json.loads(request_path.read_text()) + self.assertEqual(request["status"], "gated") + self.assertNotIn("delivered_at", request) + def test_delivered_historical_gate_may_be_human_deleted_without_blocking_poll(self) -> None: self.inventory_path.write_text(json.dumps([self.other]), encoding="utf-8") result = self.result(self.run_cli("poll")) From 83ba24e7bbd57a0660a80d73023ad3a78f14b3ac Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 16:15:58 +1000 Subject: [PATCH 17/24] fix(agent-attention): validate reminder inventory schema --- .../agent-attention/agent-attention.py | 30 +++++++++++--- .../agent-attention/test_agent_attention.py | 41 +++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index cbfa571..3e7d645 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -16,7 +16,7 @@ import uuid from datetime import datetime, timezone from pathlib import Path -from typing import Any +from typing import Any, NoReturn COMMAND_CATALOG = ( @@ -77,6 +77,20 @@ def require_json_object_array(value: Any, *, document_name: str) -> list[dict[st return value +def require_reminder_inventory(value: Any, *, document_name: str) -> list[dict[str, Any]]: + """Require reminder objects with unique nonempty text IDs.""" + inventory = require_json_object_array(value, document_name=document_name) + seen_ids: set[str] = set() + for item in inventory: + reminder_id = require_nonempty_text( + item.get("id"), field_name=f"{document_name} reminder ID" + ) + if reminder_id in seen_ids: + raise ContractError(f"{document_name} reminder IDs must be unique") + seen_ids.add(reminder_id) + return inventory + + def write_json(path: Path, value: Any, *, exclusive: bool = False) -> bool: """Write private JSON atomically enough for single-host gate custody.""" path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) @@ -567,7 +581,7 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: ) raise ContractError("created stable reminder ID did not resolve exactly once") created_gate = created_matches[0] - def fail_created_verification(field: str) -> None: + def fail_created_verification(field: str) -> NoReturn: message = f"created reminder failed exact verification: {field}" write_json( path, @@ -589,7 +603,11 @@ def fail_created_verification(field: str) -> None: ): if created_gate.get(field) != expected: fail_created_verification(field) - if not (created_gate.get("notes") or "").startswith(notes): + try: + created_notes = reminder_notes(created_gate.get("notes")) + except ContractError: + fail_created_verification("notes") + if not created_notes.startswith(notes): fail_created_verification("notes") if created_gate.get("isCompleted"): fail_created_verification("isCompleted") @@ -671,7 +689,7 @@ def read_inventory( ], timeout_seconds=timeout_seconds, ) - return require_json_object_array(inventory, document_name="remindctl inventory") + return require_reminder_inventory(inventory, document_name="remindctl inventory") def event_id(mapping: dict[str, Any]) -> str: @@ -854,7 +872,7 @@ def read_exact_completed_reminder(reminder_id: str, list_id: str) -> dict[str, A "--no-input", ] ) - inventory = require_json_object_array( + inventory = require_reminder_inventory( inventory, document_name="completed reminder inventory" ) matches = [item for item in inventory if item.get("id") == reminder_id] @@ -1055,7 +1073,7 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: config, timeout_seconds=getattr(args, "command_timeout_seconds", None), ) - items_by_id = {item.get("id"): item for item in inventory} + items_by_id = {item["id"]: item for item in inventory} for mapping_path in mapping_paths: mapping = validate_gate_mapping( diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index fef7d1c..ef633d0 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -145,6 +145,8 @@ def _write_fake_remindctl(self) -> None: "isCompleted": False, "lastModifiedDate": datetime.now(timezone.utc).isoformat(), } + if os.environ.get("FAKE_REMINDERS_NEW_NOTES_JSON"): + reminder["notes"] = json.loads(os.environ["FAKE_REMINDERS_NEW_NOTES_JSON"]) inventory.append(reminder) inventory_path.write_text(json.dumps(inventory), encoding="utf-8") print(json.dumps(reminder)) @@ -346,6 +348,30 @@ def test_poll_rejects_non_object_inventory_entry(self) -> None: self.assertIn("inventory entries must be JSON objects", completed.stderr) self.assertNotIn("Traceback", completed.stderr) + def test_poll_rejects_non_text_and_duplicate_inventory_ids(self) -> None: + for invalid_id, expected in ( + ([], "must be nonempty text"), + ({}, "must be nonempty text"), + ): + with self.subTest(invalid_id=invalid_id): + self.inventory_path.write_text( + json.dumps([self.target, {**self.other, "id": invalid_id}]), + encoding="utf-8", + ) + completed = self.run_cli("poll") + self.assertEqual(completed.returncode, 1) + self.assertIn(expected, completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + + self.inventory_path.write_text( + json.dumps([self.target, {**self.other, "id": REMINDER_ID}]), + encoding="utf-8", + ) + completed = self.run_cli("poll") + self.assertEqual(completed.returncode, 1) + self.assertIn("reminder IDs must be unique", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + def test_outcome_preview_is_read_only_and_exact(self) -> None: result = self.result( self.run_cli( @@ -861,6 +887,21 @@ def test_submit_rejects_path_like_created_reminder_id_before_mapping_write(self) ) self.assertEqual(request["status"], "repair") + def test_submit_rejects_non_text_created_notes_without_traceback(self) -> None: + completed = self.run_cli( + *self.submit_arguments(), + "--execute", + env_update={"FAKE_REMINDERS_NEW_NOTES_JSON": '{"unexpected": true}'}, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("created reminder failed exact verification: notes", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + request = json.loads( + (self.state_dir / "requests" / f"{THREAD_ID}.json").read_text() + ) + self.assertEqual(request["status"], "repair") + self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_rejects_non_object_request_state_without_traceback(self) -> None: request_dir = self.state_dir / "requests" request_dir.mkdir() From 55eb90484f3477bb50ecf1313af74cbe715547b0 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 16:25:40 +1000 Subject: [PATCH 18/24] fix(agent-attention): preserve unresolved gate attempts --- .../agent-attention/agent-attention.py | 31 +++++++++++--- .../agent-attention/test_agent_attention.py | 40 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 3e7d645..59aedfb 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -333,14 +333,35 @@ def request_path(state_dir: Path, thread_id: str) -> Path: def completed_or_active_request_result( - existing: Any, request_identifier: str, thread_id: str + state_dir: Path, existing: Any, request_identifier: str, thread_id: str ) -> dict[str, Any] | None: """Return an idempotent result or reject a distinct active gate.""" existing = require_json_object(existing, document_name="request state") status = existing.get("status") + existing_request_id = existing.get("request_id") + if not isinstance(existing_request_id, str) or len(existing_request_id) != 64 or any( + character not in "0123456789abcdef" for character in existing_request_id + ): + raise ContractError("request state request_id must be a lowercase SHA-256 digest") + if existing.get("thread_id") != thread_id: + raise ContractError("request state thread_id does not match its owner path") + unresolved_attempt = status == "declared" or ( + status == "repair" + and (state_dir / "request-claims" / f"{existing_request_id}.json").exists() + ) + if unresolved_attempt: + if existing_request_id == request_identifier: + return base_result( + "claimed", + changed=False, + request_id=request_identifier, + thread_id=thread_id, + repair="inspect exact request state before retry; no second gate was created", + ) + raise ContractError("the owning task has an unresolved gate creation attempt") if status not in {"gated", "delivered", "completed"}: return None - if existing.get("request_id") == request_identifier: + if existing_request_id == request_identifier: return base_result( "already_gated", changed=False, @@ -390,7 +411,7 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: if path.exists(): existing_result = completed_or_active_request_result( - load_json(path), request_identifier, thread_id + state_dir, load_json(path), request_identifier, thread_id ) if existing_result: return existing_result @@ -465,7 +486,7 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: if path.exists(): existing_result = completed_or_active_request_result( - load_json(path), request_identifier, thread_id + state_dir, load_json(path), request_identifier, thread_id ) if existing_result: return existing_result @@ -609,7 +630,7 @@ def fail_created_verification(field: str) -> NoReturn: fail_created_verification("notes") if not created_notes.startswith(notes): fail_created_verification("notes") - if created_gate.get("isCompleted"): + if created_gate.get("isCompleted") is not False: fail_created_verification("isCompleted") mapping = { diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index ef633d0..9c4704d 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -147,6 +147,10 @@ def _write_fake_remindctl(self) -> None: } if os.environ.get("FAKE_REMINDERS_NEW_NOTES_JSON"): reminder["notes"] = json.loads(os.environ["FAKE_REMINDERS_NEW_NOTES_JSON"]) + if os.environ.get("FAKE_REMINDERS_NEW_COMPLETION_JSON"): + reminder["isCompleted"] = json.loads(os.environ["FAKE_REMINDERS_NEW_COMPLETION_JSON"]) + if os.environ.get("FAKE_REMINDERS_OMIT_NEW_COMPLETION") == "1": + reminder.pop("isCompleted") inventory.append(reminder) inventory_path.write_text(json.dumps(inventory), encoding="utf-8") print(json.dumps(reminder)) @@ -841,6 +845,29 @@ def test_submit_recovers_request_lock_abandoned_before_creation(self) -> None: self.assertFalse(lock_path.exists()) self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_blocks_distinct_intent_while_declared_attempt_is_unresolved(self) -> None: + request_dir = self.state_dir / "requests" + request_dir.mkdir() + (request_dir / f"{THREAD_ID}.json").write_text( + json.dumps( + { + "version": 1, + "request_id": "0" * 64, + "thread_id": THREAD_ID, + "status": "declared", + } + ), + encoding="utf-8", + ) + completed = self.run_cli( + *self.submit_arguments(action="Approve a different action"), + "--execute", + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("unresolved gate creation attempt", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(self.calls(), []) + def test_failed_submit_releases_the_per_thread_request_lock(self) -> None: completed = self.run_cli( *self.submit_arguments(), @@ -902,6 +929,19 @@ def test_submit_rejects_non_text_created_notes_without_traceback(self) -> None: self.assertEqual(request["status"], "repair") self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_requires_literal_false_created_completion_state(self) -> None: + completed = self.run_cli( + *self.submit_arguments(), + "--execute", + env_update={"FAKE_REMINDERS_NEW_COMPLETION_JSON": "0"}, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn( + "created reminder failed exact verification: isCompleted", + completed.stderr, + ) + self.assertNotIn("Traceback", completed.stderr) + def test_submit_rejects_non_object_request_state_without_traceback(self) -> None: request_dir = self.state_dir / "requests" request_dir.mkdir() From 13f44c91299575c197af26d9ebc67d24266bee7c Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 16:37:43 +1000 Subject: [PATCH 19/24] fix(agent-attention): recover lifecycle crash boundaries --- .../agent-attention/agent-attention.py | 79 ++++++++++++++++++- .../agent-attention/test_agent_attention.py | 54 +++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 59aedfb..788fa91 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -37,6 +37,13 @@ class ContractError(Exception): """Raised when a gate cannot be handled without guessing.""" +class ContractArgumentParser(argparse.ArgumentParser): + """Route public usage errors through the structured result boundary.""" + + def error(self, message: str) -> NoReturn: + raise ContractError(f"invalid command arguments: {message}") + + def default_state_dir() -> Path: """Return the private user-owned runtime state directory.""" xdg_state = os.environ.get("XDG_STATE_HOME") @@ -50,8 +57,11 @@ def default_state_dir() -> Path: def load_json(path: Path) -> Any: """Load one JSON document from disk.""" - with path.open(encoding="utf-8") as handle: - return json.load(handle) + try: + with path.open(encoding="utf-8") as handle: + return json.load(handle) + except UnicodeDecodeError as error: + raise ContractError(f"persisted JSON is not valid UTF-8: {path.name}") from error def require_json_object(value: Any, *, document_name: str) -> dict[str, Any]: @@ -345,6 +355,29 @@ def completed_or_active_request_result( raise ContractError("request state request_id must be a lowercase SHA-256 digest") if existing.get("thread_id") != thread_id: raise ContractError("request state thread_id does not match its owner path") + if status == "declared": + matching_mappings = [] + for mapping_path in sorted((state_dir / "gates").glob("*.json")): + mapping = validate_gate_mapping( + load_json(mapping_path), expected_reminder_id=mapping_path.stem + ) + if ( + mapping.get("request_id") == existing_request_id + and mapping["thread_id"] == thread_id + ): + matching_mappings.append(mapping) + if len(matching_mappings) > 1: + raise ContractError("declared request resolves to multiple published gates") + if matching_mappings: + mapping = matching_mappings[0] + existing = { + **existing, + "status": "gated", + "reminder_id": mapping["reminder_id"], + "updated_at": datetime.now(timezone.utc).isoformat(), + } + write_json(request_path(state_dir, thread_id), existing) + status = "gated" unresolved_attempt = status == "declared" or ( status == "repair" and (state_dir / "request-claims" / f"{existing_request_id}.json").exists() @@ -399,6 +432,35 @@ def update_request_state( write_json(path, updated) +def reconcile_declared_request( + state_dir: Path, mapping: dict[str, Any] +) -> None: + """Bind a published gate back to request state after a publication crash.""" + request_identifier = mapping.get("request_id") + if not isinstance(request_identifier, str): + return + path = request_path(state_dir, mapping["thread_id"]) + if not path.exists(): + return + state = require_json_object(load_json(path), document_name="request state") + if state.get("status") != "declared": + return + if ( + state.get("request_id") != request_identifier + or state.get("thread_id") != mapping["thread_id"] + ): + return + write_json( + path, + { + **state, + "status": "gated", + "reminder_id": mapping["reminder_id"], + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + + def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: """Validate, deduplicate, and optionally create one native approval gate.""" state_dir: Path = args.state_dir @@ -492,6 +554,16 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: return existing_result request_claim_path = state_dir / "request-claims" / f"{request_identifier}.json" + if request_claim_path.exists() and not path.exists(): + abandoned_claim = require_json_object( + load_json(request_claim_path), document_name="request claim" + ) + if ( + abandoned_claim.get("request_id") != request_identifier + or abandoned_claim.get("thread_id") != thread_id + ): + raise ContractError("request claim does not match its exact owner") + request_claim_path.unlink() if not write_json( request_claim_path, { @@ -1100,6 +1172,7 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: mapping = validate_gate_mapping( load_json(mapping_path), expected_reminder_id=mapping_path.stem ) + reconcile_declared_request(state_dir, mapping) identifier = event_id(mapping) receipt_path = state_dir / "receipts" / f"{identifier}.json" if receipt_path.exists(): @@ -1473,7 +1546,7 @@ def commands(_: argparse.Namespace) -> dict[str, Any]: def parser() -> argparse.ArgumentParser: """Build the stable command surface.""" - command = argparse.ArgumentParser( + command = ContractArgumentParser( prog="agent-attention", description="Create and deliver bounded Apple Reminders approval gates.", ) diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 9c4704d..e509b2b 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -310,6 +310,21 @@ def test_non_object_config_uses_the_structured_error_boundary(self) -> None: self.assertEqual(completed.returncode, 1) self.assertIn("config must be a JSON object", completed.stderr) self.assertNotIn("Traceback", completed.stderr) + + def test_invalid_utf8_uses_the_structured_error_boundary(self) -> None: + (self.state_dir / "config.json").write_bytes(b"\xff") + completed = self.run_cli("poll") + self.assertEqual(completed.returncode, 1) + self.assertIn("persisted JSON is not valid UTF-8", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(json.loads(completed.stdout)["status"], "error") + + def test_argparse_usage_error_uses_the_structured_error_boundary(self) -> None: + completed = self.run_cli("submit", "--bad") + self.assertEqual(completed.returncode, 1) + self.assertIn("invalid command arguments", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(json.loads(completed.stdout)["status"], "error") result = json.loads(completed.stdout) self.assertEqual(result["status"], "error") self.assertEqual(result["error_category"], "contract_or_runtime") @@ -845,6 +860,45 @@ def test_submit_recovers_request_lock_abandoned_before_creation(self) -> None: self.assertFalse(lock_path.exists()) self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_reclaims_claim_abandoned_before_request_declaration(self) -> None: + arguments = self.submit_arguments() + preview = self.result(self.run_cli(*arguments)) + claim_dir = self.state_dir / "request-claims" + claim_dir.mkdir() + (claim_dir / f"{preview['request_id']}.json").write_text( + json.dumps( + { + "request_id": preview["request_id"], + "thread_id": THREAD_ID, + } + ), + encoding="utf-8", + ) + + result = self.result(self.run_cli(*arguments, "--execute")) + self.assertEqual(result["status"], "gated") + self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + + def test_submit_reconciles_mapping_published_before_request_update(self) -> None: + arguments = self.submit_arguments() + first = self.result(self.run_cli(*arguments, "--execute")) + request_path = self.state_dir / "requests" / f"{THREAD_ID}.json" + request = json.loads(request_path.read_text()) + request["status"] = "declared" + request.pop("reminder_id") + request_path.write_text(json.dumps(request), encoding="utf-8") + + polled = self.result(self.run_cli("poll")) + self.assertEqual(polled["status"], "waiting") + reconciled = json.loads(request_path.read_text()) + self.assertEqual(reconciled["status"], "gated") + self.assertEqual(reconciled["reminder_id"], first["reminder_id"]) + + retried = self.result(self.run_cli(*arguments, "--execute")) + self.assertEqual(retried["status"], "already_gated") + self.assertEqual(retried["reminder_id"], first["reminder_id"]) + self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_blocks_distinct_intent_while_declared_attempt_is_unresolved(self) -> None: request_dir = self.state_dir / "requests" request_dir.mkdir() From 8227ae4f477f32b4534c35899db2fad257549796 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 16:46:30 +1000 Subject: [PATCH 20/24] fix(agent-attention): preserve post-add repair state --- .../agent-attention/agent-attention.py | 17 +++++++++- .../agent-attention/test_agent_attention.py | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 788fa91..b468260 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -196,6 +196,8 @@ def run_json(command: list[str], *, timeout_seconds: float | None = None) -> Any ) except subprocess.TimeoutExpired as error: raise ContractError("command exceeded the bounded execution window") from error + except UnicodeDecodeError as error: + raise ContractError("command output is not valid UTF-8") from error if completed.returncode != 0: detail = completed.stderr.strip() or completed.stdout.strip() raise ContractError(f"command failed: {detail}") @@ -659,7 +661,20 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: }, ) raise - created_inventory = read_inventory(config) + try: + created_inventory = read_inventory(config) + except (ContractError, json.JSONDecodeError, OSError, subprocess.SubprocessError) as error: + write_json( + path, + { + **declared, + "status": "repair", + "reminder_id": reminder_id, + "repair": f"created reminder could not be verified; inspect exact stable ID before retry: {error}", + "updated_at": datetime.now(timezone.utc).isoformat(), + }, + ) + raise created_matches = [item for item in created_inventory if item.get("id") == reminder_id] if len(created_matches) != 1: write_json( diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index e509b2b..260e327 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -115,6 +115,9 @@ def _write_fake_remindctl(self) -> None: inventory = json.loads(inventory_path.read_text(encoding="utf-8")) if args[0] == "show": + if os.environ.get("FAKE_REMINDERS_INVALID_SHOW_JSON") == "1": + print("{") + raise SystemExit(0) if os.environ.get("FAKE_REMINDERS_HANG_SHOW_SECONDS"): time.sleep(float(os.environ["FAKE_REMINDERS_HANG_SHOW_SECONDS"])) if args[1] == "all" and os.environ.get("FAKE_REMINDERS_COMPLETE_AFTER_POLLS"): @@ -168,6 +171,9 @@ def _write_fake_remindctl(self) -> None: raise SystemExit(5) print(json.dumps(matches[0])) elif args[0] == "doctor": + if os.environ.get("FAKE_REMINDERS_INVALID_UTF8") == "1": + sys.stdout.buffer.write(b"\\xff") + raise SystemExit(0) print(os.environ.get("FAKE_REMINDERS_DOCTOR_JSON", '{"authorization":{"authorized":true}}')) else: print("unsupported remindctl subcommand: " + args[0], file=sys.stderr) @@ -319,6 +325,15 @@ def test_invalid_utf8_uses_the_structured_error_boundary(self) -> None: self.assertNotIn("Traceback", completed.stderr) self.assertEqual(json.loads(completed.stdout)["status"], "error") + def test_invalid_command_output_utf8_uses_the_structured_error_boundary(self) -> None: + completed = self.run_cli( + "doctor", env_update={"FAKE_REMINDERS_INVALID_UTF8": "1"} + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("command output is not valid UTF-8", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + self.assertEqual(json.loads(completed.stdout)["status"], "error") + def test_argparse_usage_error_uses_the_structured_error_boundary(self) -> None: completed = self.run_cli("submit", "--bad") self.assertEqual(completed.returncode, 1) @@ -983,6 +998,23 @@ def test_submit_rejects_non_text_created_notes_without_traceback(self) -> None: self.assertEqual(request["status"], "repair") self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_persists_stable_id_when_post_add_inventory_is_malformed(self) -> None: + completed = self.run_cli( + *self.submit_arguments(), + "--execute", + env_update={"FAKE_REMINDERS_INVALID_SHOW_JSON": "1"}, + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("command returned invalid JSON", completed.stderr) + self.assertNotIn("Traceback", completed.stderr) + request = json.loads( + (self.state_dir / "requests" / f"{THREAD_ID}.json").read_text() + ) + self.assertEqual(request["status"], "repair") + self.assertEqual(request["reminder_id"], NEW_REMINDER_ID) + self.assertIn("could not be verified", request["repair"]) + self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_requires_literal_false_created_completion_state(self) -> None: completed = self.run_cli( *self.submit_arguments(), From 1ab89e92f9cc1e9341aee42efeb77e2e93214306 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 16:54:18 +1000 Subject: [PATCH 21/24] fix(agent-attention): reclaim truncated request claims --- .../runtime/agent-attention/agent-attention.py | 10 ++-------- .../runtime/agent-attention/test_agent_attention.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index b468260..c6832c8 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -557,14 +557,8 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: request_claim_path = state_dir / "request-claims" / f"{request_identifier}.json" if request_claim_path.exists() and not path.exists(): - abandoned_claim = require_json_object( - load_json(request_claim_path), document_name="request claim" - ) - if ( - abandoned_claim.get("request_id") != request_identifier - or abandoned_claim.get("thread_id") != thread_id - ): - raise ContractError("request claim does not match its exact owner") + # Owner state is published before creation starts. Under the held per-thread + # lock, an absent owner proves even a truncated claim is safe to reclaim. request_claim_path.unlink() if not write_json( request_claim_path, diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 260e327..68d3ef8 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -894,6 +894,17 @@ def test_submit_reclaims_claim_abandoned_before_request_declaration(self) -> Non self.assertEqual(result["status"], "gated") self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_reclaims_truncated_pre_declaration_claim(self) -> None: + arguments = self.submit_arguments() + preview = self.result(self.run_cli(*arguments)) + claim_dir = self.state_dir / "request-claims" + claim_dir.mkdir() + (claim_dir / f"{preview['request_id']}.json").write_text("{", encoding="utf-8") + + result = self.result(self.run_cli(*arguments, "--execute")) + self.assertEqual(result["status"], "gated") + self.assertEqual(len([call for call in self.calls() if call[0] == "add"]), 1) + def test_submit_reconciles_mapping_published_before_request_update(self) -> None: arguments = self.submit_arguments() first = self.result(self.run_cli(*arguments, "--execute")) From a57e59de4ce0d663b0cb75bc1cd188c12c5f8e6c Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 17:01:50 +1000 Subject: [PATCH 22/24] fix(agent-attention): scan past sticky delivery claims --- .../agent-attention/agent-attention.py | 13 ++++++-- .../agent-attention/test_agent_attention.py | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index c6832c8..74c94ec 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -1176,6 +1176,7 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: timeout_seconds=getattr(args, "command_timeout_seconds", None), ) items_by_id = {item["id"]: item for item in inventory} + preserved_claim: dict[str, Any] | None = None for mapping_path in mapping_paths: mapping = validate_gate_mapping( @@ -1266,12 +1267,13 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: claim_path = state_dir / "claims" / f"{identifier}.json" if claim_path.exists(): - return base_result( + preserved_claim = preserved_claim or base_result( "claimed", changed=False, event_id=identifier, repair="inspect the destination task before releasing this claim", ) + continue claim = { "claimed_at": datetime.now(timezone.utc).isoformat(), @@ -1282,7 +1284,10 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: "approval_meaning": mapping["approval_meaning"], } if not write_json(claim_path, claim, exclusive=True): - return base_result("claimed", changed=False, event_id=identifier) + preserved_claim = preserved_claim or base_result( + "claimed", changed=False, event_id=identifier + ) + continue return base_result( "deliver", changed=True, @@ -1296,7 +1301,9 @@ def poll(args: argparse.Namespace) -> dict[str, Any]: next_safe_action="deliver once with the Codex task tool, then record-delivery", ) - return base_result("waiting", changed=False, open_gate_count=len(mapping_paths)) + return preserved_claim or base_result( + "waiting", changed=False, open_gate_count=len(mapping_paths) + ) def watch(args: argparse.Namespace) -> dict[str, Any]: diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 68d3ef8..2e67c2c 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -1337,6 +1337,38 @@ def test_poll_records_repair_for_one_broken_gate_and_delivers_the_next(self) -> self.assertEqual(broken["status"], "repair") self.assertIn(REMINDER_ID, broken["repair"]) + def test_poll_preserves_stale_claim_and_delivers_a_later_gate(self) -> None: + for path in (self.state_dir / "receipts").glob("*.json"): + path.unlink() + claim_dir = self.state_dir / "claims" + claim_dir.mkdir() + (claim_dir / f"{self._event_id()}.json").write_text("{}", encoding="utf-8") + later_meaning = "Approve the later bounded gate only." + later_mapping = { + **self.mapping, + "reminder_id": OTHER_REMINDER_ID, + "expected_title": "[APPROVE] Later bounded gate", + "required_notes_line": f"Approval meaning: {later_meaning}", + "approval_meaning": later_meaning, + } + (self.state_dir / "gates" / f"{OTHER_REMINDER_ID}.json").write_text( + json.dumps(later_mapping), encoding="utf-8" + ) + self.other.update( + { + "title": later_mapping["expected_title"], + "notes": later_mapping["required_notes_line"], + "isCompleted": True, + "completionDate": "2026-08-10T05:50:00Z", + } + ) + self._write_inventory() + + result = self.result(self.run_cli("poll")) + self.assertEqual(result["status"], "deliver") + self.assertIn(later_meaning, result["prompt"]) + self.assertTrue((claim_dir / f"{self._event_id()}.json").exists()) + def test_doctor_bounds_remindctl_and_rejects_non_object_json(self) -> None: run_json = mock.Mock(return_value=[]) with mock.patch.object(AGENT_ATTENTION, "run_json", run_json): From 5961d42366c2b824eeae04f3814d90886747a339 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 17:09:07 +1000 Subject: [PATCH 23/24] fix(agent-attention): preserve completed request state --- .../agent-attention/agent-attention.py | 2 ++ .../agent-attention/test_agent_attention.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 74c94ec..3348f97 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -422,6 +422,8 @@ def update_request_state( state = require_json_object(load_json(path), document_name="request state") if state.get("reminder_id") != mapping["reminder_id"]: return + if state.get("status") == "completed" and status != "completed": + return updated = { **state, "status": status, diff --git a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py index 2e67c2c..961206c 100644 --- a/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py +++ b/experiments/agent-attention/runtime/agent-attention/test_agent_attention.py @@ -1270,6 +1270,30 @@ def test_poll_reconciles_request_state_from_existing_delivery_receipt(self) -> N self.assertEqual(request["event_id"], self._event_id()) self.assertEqual(request["delivered_at"], "2026-08-10T05:41:00Z") + def test_poll_never_demotes_completed_request_from_existing_delivery_receipt(self) -> None: + request_dir = self.state_dir / "requests" + request_dir.mkdir() + request_path = request_dir / f"{THREAD_ID}.json" + request_path.write_text( + json.dumps( + { + "version": 1, + "request_id": "0" * 64, + "thread_id": THREAD_ID, + "reminder_id": REMINDER_ID, + "status": "completed", + "outcome_id": "1" * 64, + } + ), + encoding="utf-8", + ) + + result = self.result(self.run_cli("poll")) + self.assertEqual(result["status"], "waiting") + request = json.loads(request_path.read_text()) + self.assertEqual(request["status"], "completed") + self.assertEqual(request["outcome_id"], "1" * 64) + def test_outcome_rejects_truthy_non_boolean_completion(self) -> None: self.target["isCompleted"] = "false" self._write_inventory() From bd476c7e49455f01c4bb85d46b776e5560d9c7e4 Mon Sep 17 00:00:00 2001 From: Nathan Vale Date: Wed, 12 Aug 2026 17:16:53 +1000 Subject: [PATCH 24/24] fix(agent-attention): serialize request state transitions --- .../agent-attention/agent-attention.py | 107 ++++++++++-------- 1 file changed, 61 insertions(+), 46 deletions(-) diff --git a/experiments/agent-attention/runtime/agent-attention/agent-attention.py b/experiments/agent-attention/runtime/agent-attention/agent-attention.py index 3348f97..d84939f 100755 --- a/experiments/agent-attention/runtime/agent-attention/agent-attention.py +++ b/experiments/agent-attention/runtime/agent-attention/agent-attention.py @@ -128,7 +128,9 @@ def write_json(path: Path, value: Any, *, exclusive: bool = False) -> bool: return True -def acquire_request_lock(path: Path, value: dict[str, Any]) -> int | None: +def acquire_request_lock( + path: Path, value: dict[str, Any], *, blocking: bool = False +) -> int | None: """Acquire one crash-recoverable process-owned request lock.""" path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) flags = os.O_RDWR | os.O_CREAT @@ -136,7 +138,8 @@ def acquire_request_lock(path: Path, value: dict[str, Any]) -> int | None: flags |= os.O_NOFOLLOW descriptor = os.open(path, flags, 0o600) try: - fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) + operation = fcntl.LOCK_EX if blocking else fcntl.LOCK_EX | fcntl.LOCK_NB + fcntl.flock(descriptor, operation) except BlockingIOError: os.close(descriptor) return None @@ -419,21 +422,33 @@ def update_request_state( path = request_path(state_dir, mapping["thread_id"]) if not path.exists(): return - state = require_json_object(load_json(path), document_name="request state") - if state.get("reminder_id") != mapping["reminder_id"]: - return - if state.get("status") == "completed" and status != "completed": - return - updated = { - **state, - "status": status, - "updated_at": datetime.now(timezone.utc).isoformat(), - **values, - } - for field, value in values.items(): - if value is None: - updated.pop(field, None) - write_json(path, updated) + lock_path = state_dir / "request-locks" / f"{mapping['thread_id']}.json" + descriptor = acquire_request_lock( + lock_path, + {"thread_id": mapping["thread_id"], "operation": "update-request-state"}, + blocking=True, + ) + assert descriptor is not None + try: + if not path.exists(): + return + state = require_json_object(load_json(path), document_name="request state") + if state.get("reminder_id") != mapping["reminder_id"]: + return + if state.get("status") == "completed" and status != "completed": + return + updated = { + **state, + "status": status, + "updated_at": datetime.now(timezone.utc).isoformat(), + **values, + } + for field, value in values.items(): + if value is None: + updated.pop(field, None) + write_json(path, updated) + finally: + release_request_lock(lock_path, descriptor) def reconcile_declared_request( @@ -482,6 +497,35 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: if existing_result: return existing_result + if args.execute: + request_lock_path = state_dir / "request-locks" / f"{thread_id}.json" + request_lock_descriptor = acquire_request_lock( + request_lock_path, + { + "request_id": request_identifier, + "thread_id": thread_id, + "locked_at": datetime.now(timezone.utc).isoformat(), + }, + ) + if request_lock_descriptor is None: + return base_result( + "claimed", + changed=False, + request_id=request_identifier, + thread_id=thread_id, + repair="inspect exact request state before retry; no second gate was created", + ) + args._agent_attention_request_lock = ( + request_lock_path, + request_lock_descriptor, + ) + if path.exists(): + existing_result = completed_or_active_request_result( + state_dir, load_json(path), request_identifier, thread_id + ) + if existing_result: + return existing_result + if reasons: repair = "; ".join(reasons) if args.execute: @@ -528,35 +572,6 @@ def _submit_approval(args: argparse.Namespace) -> dict[str, Any]: next_safe_action="review the structured gate, then rerun with --execute", ) - request_lock_path = state_dir / "request-locks" / f"{thread_id}.json" - request_lock_descriptor = acquire_request_lock( - request_lock_path, - { - "request_id": request_identifier, - "thread_id": thread_id, - "locked_at": datetime.now(timezone.utc).isoformat(), - }, - ) - if request_lock_descriptor is None: - return base_result( - "claimed", - changed=False, - request_id=request_identifier, - thread_id=thread_id, - repair="inspect exact request state before retry; no second gate was created", - ) - args._agent_attention_request_lock = ( - request_lock_path, - request_lock_descriptor, - ) - - if path.exists(): - existing_result = completed_or_active_request_result( - state_dir, load_json(path), request_identifier, thread_id - ) - if existing_result: - return existing_result - request_claim_path = state_dir / "request-claims" / f"{request_identifier}.json" if request_claim_path.exists() and not path.exists(): # Owner state is published before creation starts. Under the held per-thread