@skaile/transform-proxy 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.6
4
+
5
+ - omp extension: while anon is on, in error or required, images are not sent. The extension sets omp's `images.blockImages` as a runtime override (never persisted), so omp replaces every image in the request, history included, with `Image reading is disabled.`; `/anon off` drops the override. Before, a screenshot from a browser tool made the proxy refuse every later request of the session with 400 `unsupported_content` ("Anthropic image content cannot be transformed"). `/anon status` lists `images: not sent`. An omp without the settings registry gets one warning, and the proxy keeps refusing images.
6
+
7
+ ## 0.2.5
8
+
9
+ - omp extension: the status is `🟢A` (on), `🔴A` (off), `🟠A` (error) instead of `A`, `Ⱥ`, `A!`. The crossed A was not legible in common terminal fonts, and omp strips ANSI colour from extension statuses, so the colour comes from an emoji.
10
+
3
11
  ## 0.2.4
4
12
 
5
13
  - omp extension: the status is one glyph instead of a word: `A` (on), `Ⱥ` (crossed A, off), `A!` (error; was `anon on`, `anon off`, `anon error`). It fits omp's `status` status-line segment; the README shows the `statusLine` config that puts it into the main line.
package/README.md CHANGED
@@ -93,9 +93,9 @@ The package ships an [omp](https://omp.sh) extension (subpath `@skaile/transform
93
93
  omp plugin install @skaile/transform-proxy
94
94
  ```
95
95
 
96
- It adds the command `/anon on|off|status` and a one-glyph status: `A` (on), `Ⱥ` (crossed A, off) or `A!` (error). Unless `SKAILE_TRANSFORM_PROXY_URL` names a running proxy, it needs Node ≥ 22 on `PATH` (or its path in `SKAILE_TRANSFORM_PROXY_NODE`).
96
+ It adds the command `/anon on|off|status` and a compact status: `🟢A` (on), `🔴A` (off) or `🟠A` (error). The colour is in the emoji because omp strips ANSI colour from extension statuses and draws them in its accent colour. Unless `SKAILE_TRANSFORM_PROXY_URL` names a running proxy, it needs Node ≥ 22 on `PATH` (or its path in `SKAILE_TRANSFORM_PROXY_NODE`).
97
97
 
98
- omp shows extension statuses on an extra line below the status line. To show the glyph in the status line itself, add the `status` segment and turn the extra line off in `~/.omp/agent/config.yml` (`showHookStatus: false` moves every extension status, not just this one, into the segment):
98
+ omp shows extension statuses on an extra line below the status line. To show the status in the status line itself, add the `status` segment and turn the extra line off in `~/.omp/agent/config.yml` (`showHookStatus: false` moves every extension status, not just this one, into the segment):
99
99
 
100
100
  ```yaml
101
101
  statusLine:
@@ -110,11 +110,12 @@ statusLine:
110
110
  - Otherwise it starts one proxy per omp process as a child process: the system Node (`SKAILE_TRANSFORM_PROXY_NODE`, else `node` from `PATH`) runs this package's CLI with the config lookup of the CLI (`./.skaile/transform-proxy.yaml`, then `~/.config/skaile/transform-proxy.yaml`), `--port 0 --host 127.0.0.1 --exit-with-stdin` and a random token in `SKAILE_TRANSFORM_PROXY_TOKEN`, which every routed provider sends. That config must set `defaultPipeline`. Without a config, `/anon on` refuses and says where to put one. A child that exits before printing `listening <url>`, or stays silent for 60 s, fails `/anon on` with its last stderr lines.
111
111
  - omp is a compiled Bun executable, and its resolver cannot load transform modules with native or ML dependencies (`onnxruntime-node`, `@huggingface/transformers`, `ibantools`) from `node_modules`; Node can. The child exits with omp: its stdin pipe closes when omp exits (SIGKILL included), and omp kills it on a normal exit. It does not keep omp's event loop alive. A child that dies later is replaced on the next turn.
112
112
  - Providers on a wire the proxy does not rewrite (Bedrock, Vertex, Gemini, agent bridges, providers without a base URL) are routed too, so the proxy refuses their requests: the setup fails closed. `/anon status` lists them as refused.
113
- - The session's active model is rebound to the re-routed one, so the next request already uses the new base URL. A rebind that fails is an activation failure: `/anon on` rolls the routes back, saves nothing and stays off. When anon is required, or a later turn's rebind fails while anon is on, the status shows `A!`.
113
+ - The session's active model is rebound to the re-routed one, so the next request already uses the new base URL. A rebind that fails is an activation failure: `/anon on` rolls the routes back, saves nothing and stays off. When anon is required, or a later turn's rebind fails while anon is on, the status shows `🟠A`.
114
114
  - While anon is on or in error, a `before_provider_request` handler checks every model request: a request whose model does not target the proxy gets its payload replaced with an empty object, so no conversation text leaves, and an error notice names the model. omp has no hook that cancels a provider request, and a throwing handler is only logged, so this replacement is the block.
115
+ - While anon is on or in error, images are not sent: the extension sets omp's `images.blockImages` in omp's in-memory runtime settings layer, and omp replaces every image in the request, history included, with the note `Image reading is disabled.`. The proxy cannot anonymize images and refuses every request that carries one (400 `unsupported_content`), and omp resends the whole history each turn, so a single screenshot would otherwise block the session. `/anon off` drops the override, so your own `images.blockImages` setting applies again; the override is never written to `config.yml`. An omp without this settings API gets a warning instead, and the proxy keeps refusing images.
115
116
  - The proxy session follows the root omp session: on `/new`, `/resume`, `/fork` (`session_switch`) and on a root `session_start` with a new session id, every provider is re-routed with the new `x-skaile-transform-session`. The proxy keeps the old session's state in memory until it exits; no request carries that id again. A `session_start` of a headless `print` session (how omp runs subagents) keeps the parent's id, so subagents share its pseudonyms.
116
117
  - `/anon off` restores the original base URLs and headers.
117
- - `/anon status` shows the state, the proxy URL and its source, the pipeline, the session id, and the routed and refused providers.
118
+ - `/anon status` shows the state, the proxy URL and its source, the pipeline, the session id, whether images are blocked, and the routed and refused providers.
118
119
  - The enabled flag is saved in `skaile-transform-proxy.json` in omp's agent dir (`~/.omp/agent`; `SKAILE_TRANSFORM_PROXY_OMP_STATE` overrides the path) and re-applied when a session starts.
119
120
  - `SKAILE_TRANSFORM_PROXY_MODE=required` (set by a skaile session in required mode) turns routing on at session start and refuses `/anon off`.
120
121
 
@@ -59,6 +59,10 @@ interface OmpSessionActions {
59
59
  setModel(model: OmpModel): Promise<boolean>;
60
60
  }
61
61
  interface OmpExtensionApi extends OmpSessionActions {
62
+ /** omp's package exports; `settings` is the scope its setting handles read and override. */
63
+ readonly pi?: {
64
+ readonly settings?: unknown;
65
+ };
62
66
  registerCommand(name: string, options: {
63
67
  description?: string;
64
68
  getArgumentCompletions?: (argumentPrefix: string) => OmpAutocompleteItem[] | null;
@@ -84,6 +88,14 @@ interface ProxyChildProcess {
84
88
  on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
85
89
  on(event: 'error', listener: (err: Error) => void): unknown;
86
90
  }
91
+ /**
92
+ * Image sending of the host. The proxy cannot anonymize images and refuses every request that
93
+ * carries one; omp resends the whole history each turn, so one screenshot would block the session.
94
+ */
95
+ interface ImageSending {
96
+ /** `true` stops images from reaching the provider, `false` restores the user's setting. Resolves `false` when the host offers no way to do it; that answer is final. */
97
+ setBlocked(blocked: boolean): Promise<boolean>;
98
+ }
87
99
  interface AnonControllerDeps {
88
100
  readonly env: Readonly<Record<string, string | undefined>>;
89
101
  readonly home: string;
@@ -95,6 +107,8 @@ interface AnonControllerDeps {
95
107
  readonly cliPath?: string;
96
108
  /** How long the child may take to print `listening <url>`. Default: 60 s (transform modules may load ML models). */
97
109
  readonly listenTimeoutMs?: number;
110
+ /** Blocked while anon is wanted (on, error or required), released by `/anon off`. Default: none. */
111
+ readonly images?: ImageSending;
98
112
  }
99
113
  interface AnonController {
100
114
  readonly required: boolean;
@@ -124,7 +138,7 @@ interface AnonController {
124
138
  *
125
139
  * `wanted`: model requests must go through the proxy (required mode, or `/anon on`).
126
140
  * `enabled`: every provider is routed and the active model is rebound. `wanted` without
127
- * `enabled` is the error state: the status shows `A!` and `providerRequest` refuses
141
+ * `enabled` is the error state: the status shows `🟠A` and `providerRequest` refuses
128
142
  * every request that would bypass the proxy.
129
143
  */
130
144
  export declare function createAnonController(deps: AnonControllerDeps): AnonController;
@@ -133,4 +147,6 @@ export declare function defaultStatePath(env: Readonly<Record<string, string | u
133
147
  /** Registers the `/anon` command and the event handlers on one session's extension API. */
134
148
  export declare function registerAnonExtension(pi: OmpExtensionApi, controller: AnonController): void;
135
149
  export default function anonExtension(pi: OmpExtensionApi): void;
136
- export type { AnonController, AnonControllerDeps, OmpAutocompleteItem, OmpContext, OmpEventName, OmpExtensionApi, OmpModel, OmpModelRegistry, OmpNotifyType, OmpProviderOverride, OmpSessionActions, OmpUi, ProxyChildProcess, ProxySpawnOptions };
150
+ /** Blocks images through a runtime override of omp's `images.blockImages`, which strips them from every request, history included. */
151
+ export declare function ompImageSending(pi: OmpExtensionApi, load?: () => Promise<unknown>): ImageSending;
152
+ export type { AnonController, AnonControllerDeps, ImageSending, OmpAutocompleteItem, OmpContext, OmpEventName, OmpExtensionApi, OmpModel, OmpModelRegistry, OmpNotifyType, OmpProviderOverride, OmpSessionActions, OmpUi, ProxyChildProcess, ProxySpawnOptions };
@@ -50,7 +50,7 @@ const VERBS = [
50
50
  *
51
51
  * `wanted`: model requests must go through the proxy (required mode, or `/anon on`).
52
52
  * `enabled`: every provider is routed and the active model is rebound. `wanted` without
53
- * `enabled` is the error state: the status shows `A!` and `providerRequest` refuses
53
+ * `enabled` is the error state: the status shows `🟠A` and `providerRequest` refuses
54
54
  * every request that would bypass the proxy.
55
55
  */
56
56
  function createAnonController(deps) {
@@ -62,6 +62,10 @@ function createAnonController(deps) {
62
62
  const registries = /* @__PURE__ */ new WeakMap();
63
63
  let wanted = required;
64
64
  let enabled = false;
65
+ /** Whether this controller holds the image block; only then does it release it. */
66
+ let imagesBlocked = false;
67
+ /** The host answered that it cannot block images; asked once, warned once. */
68
+ let imagesUnsupported = false;
65
69
  let endpoint;
66
70
  let starting;
67
71
  let sessionId;
@@ -265,9 +269,30 @@ function createAnonController(deps) {
265
269
  function stateLabel() {
266
270
  return enabled ? "on" : wanted ? "error" : "off";
267
271
  }
268
- /** One glyph per state, so the status fits into omp's main status line: `A` on, `Ⱥ` (crossed A) off, `A!` error. */
272
+ /** Follows `wanted`: images are blocked while model requests must go through the proxy. */
273
+ async function syncImages(ctx) {
274
+ const images = deps.images;
275
+ if (images === void 0 || imagesUnsupported || imagesBlocked === wanted) return;
276
+ let done;
277
+ try {
278
+ done = await images.setBlocked(wanted);
279
+ } catch (err) {
280
+ ctx.ui.notify(`anon: could not ${wanted ? "stop" : "restore"} image sending: ${errorMessage(err)}`, "warning");
281
+ return;
282
+ }
283
+ if (done) {
284
+ imagesBlocked = wanted;
285
+ return;
286
+ }
287
+ imagesUnsupported = true;
288
+ if (wanted) ctx.ui.notify("anon: this omp cannot stop image sending; the proxy refuses every request that carries an image", "warning");
289
+ }
290
+ /**
291
+ * Compact status for omp's main status line. omp strips ANSI colour from extension statuses and
292
+ * paints them in its accent colour, so the colour comes from an emoji: green on, red off, orange error.
293
+ */
269
294
  function showStatus(ctx) {
270
- ctx.ui.setStatus(STATUS_KEY, enabled ? "A" : wanted ? "A!" : "Ⱥ");
295
+ ctx.ui.setStatus(STATUS_KEY, enabled ? "🟢A" : wanted ? "🟠A" : "🔴A");
271
296
  }
272
297
  function statusText(ctx) {
273
298
  const registry = ctx.modelRegistry;
@@ -279,6 +304,7 @@ function createAnonController(deps) {
279
304
  lines.push(`pipeline: ${endpoint.pipeline ?? "the proxy default"}`);
280
305
  }
281
306
  if (sessionId !== void 0) lines.push(`session: ${sessionId}`);
307
+ if (imagesBlocked) lines.push("images: not sent (omp images.blockImages)");
282
308
  const routed = [...state.routed.keys()].sort();
283
309
  const refused = routed.flatMap((provider) => {
284
310
  const reason = refusalReason(apis.get(provider), state.originals.get(provider));
@@ -305,6 +331,7 @@ function createAnonController(deps) {
305
331
  ctx.ui.notify(refusalHint(), "error");
306
332
  }
307
333
  }
334
+ await syncImages(ctx);
308
335
  showStatus(ctx);
309
336
  }
310
337
  return {
@@ -347,6 +374,7 @@ function createAnonController(deps) {
347
374
  break;
348
375
  default: ctx.ui.notify(USAGE, "error");
349
376
  }
377
+ await syncImages(ctx);
350
378
  showStatus(ctx);
351
379
  },
352
380
  completions(prefix) {
@@ -542,6 +570,7 @@ function originOf(url) {
542
570
  // src/omp/index.ts
543
571
  /** omp re-imports the extension per session; the controller (proxy child, recorded originals) is process-wide. */
544
572
  const CONTROLLER_KEY = Symbol.for("@skaile/transform-proxy/omp");
573
+ const BLOCK_IMAGES = "images.blockImages";
545
574
  /** Registers the `/anon` command and the event handlers on one session's extension API. */
546
575
  function registerAnonExtension(pi, controller) {
547
576
  pi.registerCommand("anon", {
@@ -559,9 +588,35 @@ function anonExtension(pi) {
559
588
  const scope = globalThis;
560
589
  registerAnonExtension(pi, scope[CONTROLLER_KEY] ??= createAnonController({
561
590
  env: process.env,
562
- home: homedir()
591
+ home: homedir(),
592
+ images: ompImageSending(pi)
563
593
  }));
564
594
  }
595
+ /** Blocks images through a runtime override of omp's `images.blockImages`, which strips them from every request, history included. */
596
+ function ompImageSending(pi, load = loadOmpSettingsRegistry) {
597
+ let handle;
598
+ return { async setBlocked(blocked) {
599
+ const settings = pi.pi?.settings;
600
+ if (settings === void 0) return false;
601
+ handle ??= load().then(blockImagesHandle, () => void 0);
602
+ const resolved = await handle;
603
+ if (resolved === void 0) return false;
604
+ if (blocked) resolved.override(settings, true);
605
+ else resolved.clearOverride(settings);
606
+ return true;
607
+ } };
608
+ }
609
+ function loadOmpSettingsRegistry() {
610
+ return import("@oh-my-pi/pi-coding-agent/config/registry");
611
+ }
612
+ function blockImagesHandle(registry) {
613
+ if (typeof registry !== "object" || registry === null || !("lookup" in registry) || typeof registry.lookup !== "function") return void 0;
614
+ const handle = registry.lookup(BLOCK_IMAGES);
615
+ if (typeof handle !== "object" || handle === null) return void 0;
616
+ if (!("override" in handle) || typeof handle.override !== "function") return void 0;
617
+ if (!("clearOverride" in handle) || typeof handle.clearOverride !== "function") return void 0;
618
+ return handle;
619
+ }
565
620
  function on(pi, event, run) {
566
621
  pi.on(event, (_event, ctx) => guarded(ctx, () => run(ctx)));
567
622
  }
@@ -572,4 +627,4 @@ async function guarded(ctx, run) {
572
627
  ctx.ui.notify(`anon: ${errorMessage(err)}`, "error");
573
628
  }
574
629
  }
575
- export { createAnonController, anonExtension as default, defaultStatePath, registerAnonExtension };
630
+ export { createAnonController, anonExtension as default, defaultStatePath, ompImageSending, registerAnonExtension };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaile/transform-proxy",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "description": "Same-protocol passthrough proxy that runs model requests and replies through a chain of text transforms (Anthropic Messages, OpenAI chat, OpenAI responses)",
6
6
  "license": "MIT",