OMP Workbook

Read the source. Follow the evidence.

Permission-denied file fallbacks

Ada embeds OMP in a host with a real OS permission boundary. Native writes to one approved destination are denied inside the agent process, but the host has a privileged channel that can perform them.

Her obstacle is preserving native tool behavior. Reimplementing write or hashline editing would lose snapshots and bookkeeping.

She chooses a file fallback, while leaving permission policy with the host.

Milestone: delegate exact denied bytes

The public bundle provides an adapter, not an elevated writer.

Complete public TypeScript source—file-fallback.ts:

Permission-denied file fallbacks · source excerpt 1; read surrounding instructions
import * as path from "node:path";
import type { ExtensionFactory } from "@oh-my-pi/pi-coding-agent";

export interface FieldNotesWriteBroker {
    /** Real host broker: resolve only after exact bytes are durable at the supplied destination. */
    write(destination: string, content: string): Promise<void>;
}

/** Host supplies the already-canonical allowlisted destination and a real broker. No elevated writer is bundled. */
export function createFieldNotesFallback(destination: string, broker: FieldNotesWriteBroker): ExtensionFactory {
    if (!path.isAbsolute(destination)) throw new Error("The host must supply an absolute, canonical destination.");
    return pi => {
        pi.registerFileWriteFallback(async (request, ctx) => {
            if (request.sessionId !== ctx.sessionManager.getSessionId() || request.dst !== destination) return false;
            await broker.write(request.dst, request.content);
            return true;
        });
    };
}

Missing prerequisites:

  • a real broker implementation;
  • a host-established canonical allowlisted destination;
  • a real permission-denied native byte-write scenario;
  • initialized runtime installation of the fallback.

The adapter checks that the destination is absolute. It does not itself prove canonicalization; the host must perform that policy work.

It has a named factory creator and no default extension export. Import it into an SDK host; do not launch it directly with -e.

What reaches the write seam?

The seam handles native ordinary-file byte writes used by write, edit and apply_patch, including a hashline move destination, after permission errors:

  • EPERM;
  • EACCES;
  • EROFS.

A special missing-parent case recovers a denied mkdir that Bun originally surfaced as ENOENT. The broker may need to create that parent. A genuinely invalid/missing path is not automatically a permission fallback.

Requests contain:

  • dst;
  • content;
  • cause;
  • sessionId, possibly undefined outside a tool call.

Handlers run in order. The first true result means the native tool may continue as though its byte write succeeded, including native snapshot bookkeeping.

Throwing handlers are logged and skipped, including later handlers in the same extension. If none succeeds, the original error is rethrown; a recovered underlying denial may be attached as its cause.

Returning true before the bytes actually land is a correctness bug.

Path and session policy

dst is the resolved destination the failed write would target, including the final component for writes. Do not rederive it from a lexically innocent tool path.

If the destination cannot be resolved safely enough to identify it, the fallback is not consulted.

Fallback registries are process-wide. A handler can be consulted for another session’s denied write. Compare request identity to the handler’s session before prompting: ctx.ui belongs to the handler’s session, not necessarily the originator.

The public adapter intentionally refuses other sessions.

Canonical path resolution reduces specific symlink misrouting risks. It does not establish a universal sandbox or remove the need for broker-side path policy and safe filesystem operations.

Milestone: deletion remains a separate capability

A write fallback must never receive a delete request and interpret missing content as an empty file.

registerFileDeleteFallback() is separate. It covers ordinary native unlink paths such as hashline REM, a move’s source unlink and patch deletion.

A delete request contains:

  • dst;
  • cause;
  • confirmedFile;
  • sessionId.

The final path component is not resolved through a symlink: unlink removes the link itself.

A broker must use plain unlink semantics. It must never fall back to recursive removal or realpath the final component.

On Darwin, unlinking a directory can report EPERM. The seam refuses a known directory, but inaccessible metadata can leave the target’s type unknown. confirmedFile: false can mean either unknown metadata or a symlink—not permission to recursively remove it.

This complete adapter exercise chooses an even narrower policy: only positively identified regular files.

Complete TypeScript host-adapter exercise—not an elevated implementation:

Permission-denied file fallbacks · source excerpt 2; read surrounding instructions
import * as path from "node:path";
import type { ExtensionFactory } from "@oh-my-pi/pi-coding-agent";

export interface PlainUnlinkBroker {
    unlink(destination: string): Promise<void>;
}

export function createSingleFileDeleteFallback(
    destination: string,
    broker: PlainUnlinkBroker,
): ExtensionFactory {
    if (!path.isAbsolute(destination)) throw new Error("An absolute policy destination is required.");
    return pi => {
        pi.registerFileDeleteFallback(async (request, ctx) => {
            if (request.sessionId !== ctx.sessionManager.getSessionId()) return false;
            if (request.dst !== destination || !request.confirmedFile) return false;
            await broker.unlink(request.dst);
            return true;
        });
    };
}

That stricter confirmedFile choice deliberately declines symlinks and metadata-hidden targets. A different broker policy may support them with safe plain-unlink operations, but recursive deletion is never the fallback contract.

ENOENT on delete is not diverted.

What is not covered

These APIs do not intercept:

  • arbitrary Bun.write() or fs calls made by extensions;
  • shell/subprocess writes;
  • archive-member rewrites;
  • SQLite row writes;
  • ACP client-side writeTextFile;
  • the LSP tool’s independent workspace-edit/code-action writes;
  • formatter subprocess mutations.

Register fallback handlers during factory loading. They are installed at runner initialization and disposed on shutdown. A first registration after initialization does not install a previously absent seam.

Each invocation receives a newly built context, so current cwd/UI information is not frozen at installation.

Observed scope: existing contract tests exercised real permission-denied native writes and follow-up editing. The test’s stand-in broker temporarily changed permissions to place bytes; it was not a real privileged broker. The workbook’s example scenarios did not exercise elevation.

Exercise: the broker succeeded at the move destination but source unlink failed. Is the move an atomic success?

Answer: no. The two primitives have separate outcomes. Inspect the actual files before retrying.

Source, snapshot 2026-08-29: packages/coding-agent/src/tools/file-write-fallback.ts; packages/coding-agent/src/extensibility/extensions/runner.ts, initialization/disposal; packages/coding-agent/src/extensibility/extensions/wrapper.ts, file-mutation session attribution; packages/coding-agent/test/sdk-file-write-fallback-extension.test.ts.

Extensions inside those boundaries · Source chapter: extensions/permission-denied-file-fallbacks. Original evidence remains scoped to its recorded snapshot.

Read this chapter as Markdown

Your lesson ticks

A self-reported reading checklist, not proof of real OMP behavior. Only these ticks are saved in this browser. Reading a milestone does not resume, fork, reset or export a session.

Chapters I have worked through
Start here 1
Sessions, resets, and reviewable history 19
Memory and reusable knowledge 14
Tangent work and live control 17
Tool permissions and approvals 15
Extensions inside those boundaries 23
Connections and next steps 8
0 of 97 checked

Checklist saving needs JavaScript and available browser storage.