codex-channel-bgos 0.2.0

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.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/adapter.d.ts +96 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +677 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/agent-hints.d.ts +20 -0
  8. package/dist/agent-hints.d.ts.map +1 -0
  9. package/dist/agent-hints.js +92 -0
  10. package/dist/agent-hints.js.map +1 -0
  11. package/dist/agent-identity.d.ts +36 -0
  12. package/dist/agent-identity.d.ts.map +1 -0
  13. package/dist/agent-identity.js +42 -0
  14. package/dist/agent-identity.js.map +1 -0
  15. package/dist/attachment-bridge.d.ts +64 -0
  16. package/dist/attachment-bridge.d.ts.map +1 -0
  17. package/dist/attachment-bridge.js +216 -0
  18. package/dist/attachment-bridge.js.map +1 -0
  19. package/dist/auth-mode.d.ts +34 -0
  20. package/dist/auth-mode.d.ts.map +1 -0
  21. package/dist/auth-mode.js +48 -0
  22. package/dist/auth-mode.js.map +1 -0
  23. package/dist/bgos-api.d.ts +191 -0
  24. package/dist/bgos-api.d.ts.map +1 -0
  25. package/dist/bgos-api.js +225 -0
  26. package/dist/bgos-api.js.map +1 -0
  27. package/dist/bgos-ws.d.ts +94 -0
  28. package/dist/bgos-ws.d.ts.map +1 -0
  29. package/dist/bgos-ws.js +310 -0
  30. package/dist/bgos-ws.js.map +1 -0
  31. package/dist/capabilities.d.ts +28 -0
  32. package/dist/capabilities.d.ts.map +1 -0
  33. package/dist/capabilities.js +44 -0
  34. package/dist/capabilities.js.map +1 -0
  35. package/dist/catalog-sync.d.ts +26 -0
  36. package/dist/catalog-sync.d.ts.map +1 -0
  37. package/dist/catalog-sync.js +24 -0
  38. package/dist/catalog-sync.js.map +1 -0
  39. package/dist/cli.d.ts +3 -0
  40. package/dist/cli.d.ts.map +1 -0
  41. package/dist/cli.js +152 -0
  42. package/dist/cli.js.map +1 -0
  43. package/dist/codex-host.d.ts +44 -0
  44. package/dist/codex-host.d.ts.map +1 -0
  45. package/dist/codex-host.js +142 -0
  46. package/dist/codex-host.js.map +1 -0
  47. package/dist/commands-sync.d.ts +24 -0
  48. package/dist/commands-sync.d.ts.map +1 -0
  49. package/dist/commands-sync.js +43 -0
  50. package/dist/commands-sync.js.map +1 -0
  51. package/dist/config.d.ts +4 -0
  52. package/dist/config.d.ts.map +1 -0
  53. package/dist/config.js +36 -0
  54. package/dist/config.js.map +1 -0
  55. package/dist/default-commands.d.ts +27 -0
  56. package/dist/default-commands.d.ts.map +1 -0
  57. package/dist/default-commands.js +64 -0
  58. package/dist/default-commands.js.map +1 -0
  59. package/dist/event-mapper.d.ts +43 -0
  60. package/dist/event-mapper.d.ts.map +1 -0
  61. package/dist/event-mapper.js +127 -0
  62. package/dist/event-mapper.js.map +1 -0
  63. package/dist/heartbeat.d.ts +75 -0
  64. package/dist/heartbeat.d.ts.map +1 -0
  65. package/dist/heartbeat.js +186 -0
  66. package/dist/heartbeat.js.map +1 -0
  67. package/dist/inbound-handler.d.ts +167 -0
  68. package/dist/inbound-handler.d.ts.map +1 -0
  69. package/dist/inbound-handler.js +282 -0
  70. package/dist/inbound-handler.js.map +1 -0
  71. package/dist/inbound-input.d.ts +17 -0
  72. package/dist/inbound-input.d.ts.map +1 -0
  73. package/dist/inbound-input.js +25 -0
  74. package/dist/inbound-input.js.map +1 -0
  75. package/dist/index.d.ts +25 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +18 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/install-service.d.ts +8 -0
  80. package/dist/install-service.d.ts.map +1 -0
  81. package/dist/install-service.js +66 -0
  82. package/dist/install-service.js.map +1 -0
  83. package/dist/last-id-store.d.ts +33 -0
  84. package/dist/last-id-store.d.ts.map +1 -0
  85. package/dist/last-id-store.js +101 -0
  86. package/dist/last-id-store.js.map +1 -0
  87. package/dist/load-config.d.ts +30 -0
  88. package/dist/load-config.d.ts.map +1 -0
  89. package/dist/load-config.js +86 -0
  90. package/dist/load-config.js.map +1 -0
  91. package/dist/media-classify.d.ts +37 -0
  92. package/dist/media-classify.d.ts.map +1 -0
  93. package/dist/media-classify.js +108 -0
  94. package/dist/media-classify.js.map +1 -0
  95. package/dist/media-guard.d.ts +13 -0
  96. package/dist/media-guard.d.ts.map +1 -0
  97. package/dist/media-guard.js +159 -0
  98. package/dist/media-guard.js.map +1 -0
  99. package/dist/outbound-retry.d.ts +24 -0
  100. package/dist/outbound-retry.d.ts.map +1 -0
  101. package/dist/outbound-retry.js +75 -0
  102. package/dist/outbound-retry.js.map +1 -0
  103. package/dist/outbound.d.ts +223 -0
  104. package/dist/outbound.d.ts.map +1 -0
  105. package/dist/outbound.js +366 -0
  106. package/dist/outbound.js.map +1 -0
  107. package/dist/outbox.d.ts +16 -0
  108. package/dist/outbox.d.ts.map +1 -0
  109. package/dist/outbox.js +88 -0
  110. package/dist/outbox.js.map +1 -0
  111. package/dist/pair-cli.d.ts +44 -0
  112. package/dist/pair-cli.d.ts.map +1 -0
  113. package/dist/pair-cli.js +182 -0
  114. package/dist/pair-cli.js.map +1 -0
  115. package/dist/pending-unknown-store.d.ts +26 -0
  116. package/dist/pending-unknown-store.d.ts.map +1 -0
  117. package/dist/pending-unknown-store.js +135 -0
  118. package/dist/pending-unknown-store.js.map +1 -0
  119. package/dist/processed-ids.d.ts +36 -0
  120. package/dist/processed-ids.d.ts.map +1 -0
  121. package/dist/processed-ids.js +55 -0
  122. package/dist/processed-ids.js.map +1 -0
  123. package/dist/reply-markers.d.ts +42 -0
  124. package/dist/reply-markers.d.ts.map +1 -0
  125. package/dist/reply-markers.js +124 -0
  126. package/dist/reply-markers.js.map +1 -0
  127. package/dist/setup/supervisor.d.ts +16 -0
  128. package/dist/setup/supervisor.d.ts.map +1 -0
  129. package/dist/setup/supervisor.js +61 -0
  130. package/dist/setup/supervisor.js.map +1 -0
  131. package/dist/thread-map.d.ts +12 -0
  132. package/dist/thread-map.d.ts.map +1 -0
  133. package/dist/thread-map.js +72 -0
  134. package/dist/thread-map.js.map +1 -0
  135. package/dist/tool-progress.d.ts +103 -0
  136. package/dist/tool-progress.d.ts.map +1 -0
  137. package/dist/tool-progress.js +216 -0
  138. package/dist/tool-progress.js.map +1 -0
  139. package/dist/types.d.ts +244 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +16 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/version.d.ts +2 -0
  144. package/dist/version.d.ts.map +1 -0
  145. package/dist/version.js +28 -0
  146. package/dist/version.js.map +1 -0
  147. package/dist/voice-rpc.d.ts +37 -0
  148. package/dist/voice-rpc.d.ts.map +1 -0
  149. package/dist/voice-rpc.js +44 -0
  150. package/dist/voice-rpc.js.map +1 -0
  151. package/package.json +47 -0
@@ -0,0 +1,30 @@
1
+ import type { CatalogAgent } from "./catalog-sync.js";
2
+ import type { PluginConfig } from "./types.js";
3
+ export interface LoadedConfig extends PluginConfig {
4
+ agents?: CatalogAgent[];
5
+ }
6
+ /**
7
+ * Synchronous, never-throwing read of the pairing secrets written by
8
+ * `codex-pair-bgos` (`~/.codex-bgos/secrets/bgos.json`). Returns `null` when the
9
+ * file is missing, unreadable, malformed, or the token is too short.
10
+ *
11
+ * Intended for constructors that cannot `await` (e.g. `BgosProactiveClient`),
12
+ * so the SEPARATE proactive check-in/briefing processes - which do not inherit
13
+ * the adapter's `CODEX_BGOS_PAIRING_TOKEN` env - can still authenticate off the same
14
+ * secrets file the long-running adapter uses. Safe to call unconditionally; a
15
+ * missing file simply yields `null`.
16
+ */
17
+ export declare function readSecretsSafe(): {
18
+ pairingToken?: string;
19
+ baseUrl?: string;
20
+ } | null;
21
+ /**
22
+ * Load BGOS plugin config from (in order of precedence):
23
+ * 1. `~/.codex-bgos/secrets/bgos.json` (written by `codex-pair-bgos`)
24
+ * 2. `BGOS_PAIRING_TOKEN` env var
25
+ * Plus optional agent catalog from `CODEX_BGOS_AGENTS=route:Name,route:Name,...`.
26
+ *
27
+ * Throws if neither secrets file nor env var supplies a valid pairing token.
28
+ */
29
+ export declare function loadConfig(): LoadedConfig;
30
+ //# sourceMappingURL=load-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,YAAa,SAAQ,YAAY;IAChD,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACzB;AAgCD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,IAAI;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,IAAI,CAMP;AAgBD;;;;;;;GAOG;AACH,wBAAgB,UAAU,IAAI,YAAY,CAazC"}
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { loadConfigFromEnv, loadConfigFromPluginCfg } from "./config.js";
5
+ function resolveCodexBgosHome() {
6
+ const fromEnv = process.env.CODEX_BGOS_HOME?.trim();
7
+ if (fromEnv) {
8
+ if (fromEnv.startsWith("~")) {
9
+ return join(homedir(), fromEnv.slice(1));
10
+ }
11
+ return fromEnv;
12
+ }
13
+ return join(homedir(), ".codex-bgos");
14
+ }
15
+ function readSecrets() {
16
+ const path = join(resolveCodexBgosHome(), "secrets", "bgos.json");
17
+ try {
18
+ const raw = readFileSync(path, "utf8");
19
+ const parsed = JSON.parse(raw);
20
+ if (parsed?.pairingToken && parsed.pairingToken.length >= 20) {
21
+ return parsed;
22
+ }
23
+ }
24
+ catch {
25
+ // file missing / unreadable - fall through to env
26
+ }
27
+ return null;
28
+ }
29
+ /**
30
+ * Synchronous, never-throwing read of the pairing secrets written by
31
+ * `codex-pair-bgos` (`~/.codex-bgos/secrets/bgos.json`). Returns `null` when the
32
+ * file is missing, unreadable, malformed, or the token is too short.
33
+ *
34
+ * Intended for constructors that cannot `await` (e.g. `BgosProactiveClient`),
35
+ * so the SEPARATE proactive check-in/briefing processes - which do not inherit
36
+ * the adapter's `CODEX_BGOS_PAIRING_TOKEN` env - can still authenticate off the same
37
+ * secrets file the long-running adapter uses. Safe to call unconditionally; a
38
+ * missing file simply yields `null`.
39
+ */
40
+ export function readSecretsSafe() {
41
+ try {
42
+ return readSecrets();
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function parseAgents(raw) {
49
+ if (!raw)
50
+ return [];
51
+ return raw
52
+ .split(",")
53
+ .map((s) => s.trim())
54
+ .filter(Boolean)
55
+ .map((entry) => {
56
+ const [route, name] = entry.split(":").map((p) => p.trim());
57
+ if (!route)
58
+ return null;
59
+ return { route, name: name || route };
60
+ })
61
+ .filter((a) => a !== null);
62
+ }
63
+ /**
64
+ * Load BGOS plugin config from (in order of precedence):
65
+ * 1. `~/.codex-bgos/secrets/bgos.json` (written by `codex-pair-bgos`)
66
+ * 2. `BGOS_PAIRING_TOKEN` env var
67
+ * Plus optional agent catalog from `CODEX_BGOS_AGENTS=route:Name,route:Name,...`.
68
+ *
69
+ * Throws if neither secrets file nor env var supplies a valid pairing token.
70
+ */
71
+ export function loadConfig() {
72
+ const secrets = readSecrets();
73
+ let base;
74
+ if (secrets?.pairingToken) {
75
+ base = loadConfigFromPluginCfg({
76
+ pairingToken: secrets.pairingToken,
77
+ baseUrl: secrets.baseUrl,
78
+ });
79
+ }
80
+ else {
81
+ base = loadConfigFromEnv();
82
+ }
83
+ const agents = parseAgents(process.env.CODEX_BGOS_AGENTS);
84
+ return { ...base, agents };
85
+ }
86
+ //# sourceMappingURL=load-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config.js","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Outbound attachment classification + intrinsic-dimension sniffing.
3
+ *
4
+ * Why this exists: the BGOS backend stores `isImage`/`isVideo`/`isAudio`/
5
+ * `isDocument` on a message_file VERBATIM - it does NOT derive them from the
6
+ * MIME type (`row.isImage = dto.isImage ?? null`). The frontend renders a
7
+ * file as an inline image/video ONLY when the matching flag is `true`;
8
+ * otherwise it falls back to a generic document card. So an outbound image
9
+ * sent WITHOUT `isImage: true` silently renders as a non-image download card
10
+ * (the live "agent images don't show in BGOS" bug, 2026-05-31). Every
11
+ * outbound attachment must therefore carry these flags.
12
+ */
13
+ export interface MediaKindFlags {
14
+ isImage: boolean;
15
+ isVideo: boolean;
16
+ isAudio: boolean;
17
+ isDocument: boolean;
18
+ }
19
+ /** Map a MIME type to BGOS's four mutually-exclusive attachment-kind flags. */
20
+ export declare function classifyMedia(mime: string): MediaKindFlags;
21
+ /** Wrap raw base64 as a data URI. The client feeds inline `fileData` straight
22
+ * into `<Image source={{ uri }}>`, which needs a real URI - bare base64
23
+ * won't load. (The `/send-message` path also accepts/coerces this; the
24
+ * `/messages` path stores it verbatim and renders it directly.) */
25
+ export declare function toInlineDataUri(mime: string, base64: string): string;
26
+ /**
27
+ * Best-effort intrinsic (width, height) for the common image formats,
28
+ * dependency-free (no image library). Returns `{}` for anything it can't
29
+ * parse - width/height are optional (the frontend measures the image when
30
+ * they're absent), so a miss only costs a brief initial-layout aspect
31
+ * imprecision. Fully guarded: a malformed header yields `{}`, never throws.
32
+ */
33
+ export declare function sniffImageDimensions(data: Uint8Array): {
34
+ width?: number;
35
+ height?: number;
36
+ };
37
+ //# sourceMappingURL=media-classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-classify.d.ts","sourceRoot":"","sources":["../src/media-classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAa1D;AAED;;;oEAGoE;AACpE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,UAAU,GACf;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAqErC"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Outbound attachment classification + intrinsic-dimension sniffing.
3
+ *
4
+ * Why this exists: the BGOS backend stores `isImage`/`isVideo`/`isAudio`/
5
+ * `isDocument` on a message_file VERBATIM - it does NOT derive them from the
6
+ * MIME type (`row.isImage = dto.isImage ?? null`). The frontend renders a
7
+ * file as an inline image/video ONLY when the matching flag is `true`;
8
+ * otherwise it falls back to a generic document card. So an outbound image
9
+ * sent WITHOUT `isImage: true` silently renders as a non-image download card
10
+ * (the live "agent images don't show in BGOS" bug, 2026-05-31). Every
11
+ * outbound attachment must therefore carry these flags.
12
+ */
13
+ /** Map a MIME type to BGOS's four mutually-exclusive attachment-kind flags. */
14
+ export function classifyMedia(mime) {
15
+ const m = (mime || "").toLowerCase();
16
+ const isImage = m.startsWith("image/");
17
+ const isVideo = m.startsWith("video/");
18
+ const isAudio = m.startsWith("audio/");
19
+ return {
20
+ isImage,
21
+ isVideo,
22
+ isAudio,
23
+ // Anything that isn't recognized media is a document (the download-card
24
+ // fallback). Mutually exclusive with the three above.
25
+ isDocument: !(isImage || isVideo || isAudio),
26
+ };
27
+ }
28
+ /** Wrap raw base64 as a data URI. The client feeds inline `fileData` straight
29
+ * into `<Image source={{ uri }}>`, which needs a real URI - bare base64
30
+ * won't load. (The `/send-message` path also accepts/coerces this; the
31
+ * `/messages` path stores it verbatim and renders it directly.) */
32
+ export function toInlineDataUri(mime, base64) {
33
+ return `data:${mime};base64,${base64}`;
34
+ }
35
+ /**
36
+ * Best-effort intrinsic (width, height) for the common image formats,
37
+ * dependency-free (no image library). Returns `{}` for anything it can't
38
+ * parse - width/height are optional (the frontend measures the image when
39
+ * they're absent), so a miss only costs a brief initial-layout aspect
40
+ * imprecision. Fully guarded: a malformed header yields `{}`, never throws.
41
+ */
42
+ export function sniffImageDimensions(data) {
43
+ const b = data;
44
+ try {
45
+ const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
46
+ // PNG: 8-byte sig, then an IHDR chunk with width/height as big-endian u32.
47
+ if (b.length >= 24 &&
48
+ b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47 &&
49
+ b[12] === 0x49 && b[13] === 0x48 && b[14] === 0x44 && b[15] === 0x52) {
50
+ return { width: dv.getUint32(16), height: dv.getUint32(20) };
51
+ }
52
+ // GIF: 'GIF8' then logical-screen width/height little-endian.
53
+ if (b.length >= 10 && b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46) {
54
+ return { width: dv.getUint16(6, true), height: dv.getUint16(8, true) };
55
+ }
56
+ // JPEG: scan for a Start-Of-Frame (SOFn) marker carrying dimensions.
57
+ if (b.length >= 4 && b[0] === 0xff && b[1] === 0xd8) {
58
+ let i = 2;
59
+ while (i + 9 < b.length) {
60
+ if (b[i] !== 0xff) {
61
+ i++;
62
+ continue;
63
+ }
64
+ const marker = b[i + 1];
65
+ i += 2;
66
+ // SOF0..SOF15 except DHT(C4)/JPG(C8)/DAC(CC) carry the frame size.
67
+ if (marker >= 0xc0 && marker <= 0xcf &&
68
+ marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
69
+ return { height: dv.getUint16(i + 3), width: dv.getUint16(i + 5) };
70
+ }
71
+ if (i + 2 > b.length)
72
+ break;
73
+ i += dv.getUint16(i); // skip this segment
74
+ }
75
+ return {};
76
+ }
77
+ // WebP (RIFF container): VP8 (lossy) / VP8L (lossless) / VP8X (extended).
78
+ if (b.length >= 30 &&
79
+ b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 &&
80
+ b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50) {
81
+ const fmt = String.fromCharCode(b[12], b[13], b[14], b[15]);
82
+ if (fmt === "VP8 ") {
83
+ return {
84
+ width: dv.getUint16(26, true) & 0x3fff,
85
+ height: dv.getUint16(28, true) & 0x3fff,
86
+ };
87
+ }
88
+ if (fmt === "VP8L") {
89
+ const b1 = b[21], b2 = b[22], b3 = b[23], b4 = b[24];
90
+ return {
91
+ width: (((b2 & 0x3f) << 8) | b1) + 1,
92
+ height: (((b4 & 0x0f) << 10) | (b3 << 2) | ((b2 & 0xc0) >> 6)) + 1,
93
+ };
94
+ }
95
+ if (fmt === "VP8X") {
96
+ return {
97
+ width: (b[24] | (b[25] << 8) | (b[26] << 16)) + 1,
98
+ height: (b[27] | (b[28] << 8) | (b[29] << 16)) + 1,
99
+ };
100
+ }
101
+ }
102
+ }
103
+ catch {
104
+ return {};
105
+ }
106
+ return {};
107
+ }
108
+ //# sourceMappingURL=media-classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-classify.js","sourceRoot":"","sources":["../src/media-classify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /** Thrown when an outbound file path fails the allowlist. */
2
+ export declare class MediaPathError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /**
6
+ * Validate an agent-supplied outbound file path against the allowlist and
7
+ * return its resolved, symlink-free real path. Throws `MediaPathError` on
8
+ * any rejection - the caller MUST NOT publish when this throws.
9
+ *
10
+ * @param filePath agent-supplied path (absolute or relative to cwd)
11
+ */
12
+ export declare function resolveAllowedMediaPath(filePath: string): string;
13
+ //# sourceMappingURL=media-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-guard.d.ts","sourceRoot":"","sources":["../src/media-guard.ts"],"names":[],"mappings":"AAkCA,6DAA6D;AAC7D,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAuED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoDhE"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Outbound media-path guard.
3
+ *
4
+ * SECURITY (defense-in-depth): every outbound file send - `sendFile`,
5
+ * `sendImage`, `sendVideo`, and the structured `uploadFile` - ultimately
6
+ * hands an agent-supplied `filePath` to `publishMediaPath`, which reads it
7
+ * off disk and ships its bytes to BGOS. Without a guard, a compromised /
8
+ * misbehaving agent could exfiltrate ANY file the host process can read
9
+ * (`/etc/passwd`, `~/.ssh/id_rsa`, app secrets, etc.).
10
+ *
11
+ * The BGOS backend is now the sole authority for chat/session resolution,
12
+ * but it can't see the host filesystem, so the path allowlist has to live
13
+ * here, at the boundary, before a single byte is read.
14
+ *
15
+ * Policy
16
+ * ------
17
+ * - Resolve the requested path to an absolute, symlink-free real path.
18
+ * - Reject anything that isn't inside the allowed media root.
19
+ * - The allowed root is `CODEX_BGOS_MEDIA_ROOT` (resolved + symlink-free);
20
+ * when unset it defaults to `<cwd>/media`, falling back to `<cwd>`
21
+ * if `<cwd>/media` doesn't exist - so an out-of-the-box install can
22
+ * still send files the agent wrote under the working dir, while a
23
+ * hardened deployment can pin a narrow root.
24
+ * - Reject obviously-sensitive locations outright even on the off chance
25
+ * the root is mis-configured to a parent of them (belt + suspenders).
26
+ * - Reject symlinks whose real target escapes the root (handled by
27
+ * realpath-ing both sides before the containment check).
28
+ *
29
+ * On rejection we throw a clear `MediaPathError`; callers must NOT publish.
30
+ */
31
+ import { realpathSync } from "node:fs";
32
+ import { homedir } from "node:os";
33
+ import { isAbsolute, resolve, sep } from "node:path";
34
+ /** Thrown when an outbound file path fails the allowlist. */
35
+ export class MediaPathError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = "MediaPathError";
39
+ }
40
+ }
41
+ /**
42
+ * Absolute prefixes that are never sendable regardless of the configured
43
+ * root - a second line of defense in case `CODEX_BGOS_MEDIA_ROOT` is pointed
44
+ * (mis-configured) at `/` or a home dir. Compared against the resolved
45
+ * real path, case-sensitively (matches POSIX; the dominant deploy target).
46
+ */
47
+ const SENSITIVE_PREFIXES = [
48
+ "/etc",
49
+ "/proc",
50
+ "/sys",
51
+ "/dev",
52
+ "/root",
53
+ "/var/run",
54
+ "/run/secrets",
55
+ // Common secret-bearing dot-dirs under the host user's home.
56
+ resolve(homedir(), ".ssh"),
57
+ resolve(homedir(), ".aws"),
58
+ resolve(homedir(), ".gnupg"),
59
+ resolve(homedir(), ".config", "gcloud"),
60
+ resolve(homedir(), ".kube"),
61
+ // The plugin's own pairing-token store.
62
+ resolve(homedir(), ".codex-bgos", "secrets"),
63
+ ];
64
+ /**
65
+ * Resolve the allowed media root.
66
+ *
67
+ * `CODEX_BGOS_MEDIA_ROOT` wins when set. Otherwise we prefer `<cwd>/media`
68
+ * (the conventional drop dir) and fall back to `<cwd>` when that folder
69
+ * doesn't exist. The returned path is realpath-resolved so symlinked
70
+ * roots are compared by their true location.
71
+ */
72
+ function resolveMediaRoot() {
73
+ const raw = process.env.CODEX_BGOS_MEDIA_ROOT?.trim();
74
+ if (raw) {
75
+ return realpathOrResolve(raw);
76
+ }
77
+ const cwd = process.cwd();
78
+ const mediaDir = resolve(cwd, "media");
79
+ try {
80
+ // If `<cwd>/media` exists, pin to it (narrower = safer).
81
+ return realpathSync(mediaDir);
82
+ }
83
+ catch {
84
+ // No media dir - fall back to the working directory itself.
85
+ return realpathOrResolve(cwd);
86
+ }
87
+ }
88
+ /** realpath a path if it exists; otherwise return its absolute resolution.
89
+ * Used for the ROOT (which may legitimately not exist yet) - the target
90
+ * file is realpath'd separately and must exist. */
91
+ function realpathOrResolve(p) {
92
+ const abs = isAbsolute(p) ? p : resolve(process.cwd(), p);
93
+ try {
94
+ return realpathSync(abs);
95
+ }
96
+ catch {
97
+ return abs;
98
+ }
99
+ }
100
+ /** True when `child` is the same as, or nested under, `parent`. Both must
101
+ * already be absolute + normalized. Uses a separator-terminated compare
102
+ * so `/a/media` does not match `/a/media-evil`. */
103
+ function isContained(parent, child) {
104
+ if (child === parent)
105
+ return true;
106
+ const withSep = parent.endsWith(sep) ? parent : parent + sep;
107
+ return child.startsWith(withSep);
108
+ }
109
+ /**
110
+ * Validate an agent-supplied outbound file path against the allowlist and
111
+ * return its resolved, symlink-free real path. Throws `MediaPathError` on
112
+ * any rejection - the caller MUST NOT publish when this throws.
113
+ *
114
+ * @param filePath agent-supplied path (absolute or relative to cwd)
115
+ */
116
+ export function resolveAllowedMediaPath(filePath) {
117
+ if (typeof filePath !== "string" || filePath.trim() === "") {
118
+ throw new MediaPathError("outbound file path is empty");
119
+ }
120
+ // Cheap pre-check: reject NUL bytes (path-truncation tricks) and any
121
+ // explicit parent-traversal segment before we touch the filesystem.
122
+ if (filePath.includes("\0")) {
123
+ throw new MediaPathError("outbound file path contains a NUL byte");
124
+ }
125
+ const requested = isAbsolute(filePath)
126
+ ? filePath
127
+ : resolve(process.cwd(), filePath);
128
+ // realpath the target - this both proves the file exists AND collapses
129
+ // any `..` segments and symlink hops to the true on-disk location, so a
130
+ // symlink under the root that points outside it is caught by the
131
+ // containment check below.
132
+ let real;
133
+ try {
134
+ real = realpathSync(requested);
135
+ }
136
+ catch {
137
+ throw new MediaPathError(`outbound file not found or unreadable: ${filePath}`);
138
+ }
139
+ // Hard-deny sensitive locations regardless of root configuration. We
140
+ // check BOTH the pre-resolution absolute path AND the realpath, because:
141
+ // - the realpath catches a symlink that points INTO a sensitive dir;
142
+ // - the requested path catches the literal request on platforms where
143
+ // the sensitive dir is itself a symlink (e.g. macOS `/etc` ->
144
+ // `/private/etc`), which realpath would otherwise rewrite past our
145
+ // prefix list.
146
+ for (const prefix of SENSITIVE_PREFIXES) {
147
+ if (isContained(prefix, real) || isContained(prefix, requested)) {
148
+ throw new MediaPathError(`refusing to send file from a sensitive location: ${real}`);
149
+ }
150
+ }
151
+ // Must live under the allowed media root.
152
+ const root = resolveMediaRoot();
153
+ if (!isContained(root, real)) {
154
+ throw new MediaPathError(`outbound file path ${real} is outside the allowed media root ${root} ` +
155
+ `(set CODEX_BGOS_MEDIA_ROOT to widen it)`);
156
+ }
157
+ return real;
158
+ }
159
+ //# sourceMappingURL=media-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-guard.js","sourceRoot":"","sources":["../src/media-guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Classify an outbound send failure (contract C3 outbound).
3
+ *
4
+ * Retry (backoff 1s/5s/25s) ONLY for the provably-undelivered network-error
5
+ * class, because the backend message insert is NOT idempotent:
6
+ * - connection-level errors that fail BEFORE the request reaches the server
7
+ * (ECONNREFUSED, ENOTFOUND, EAI_AGAIN, EHOSTUNREACH, ENETUNREACH,
8
+ * socket-hang-up-before-response)
9
+ * - HTTP 429 (honor Retry-After)
10
+ *
11
+ * Everything else is AMBIGUOUS (the request may have been applied server-side):
12
+ * request timeouts (ECONNABORTED) and 5xx get NO retry, and every other 4xx is
13
+ * a caller error. Ambiguous failures reject to the caller and set a heartbeat
14
+ * lastError, but are never retried or spooled.
15
+ */
16
+ export interface OutboundErrorClass {
17
+ retriable: boolean;
18
+ /** When set (429 Retry-After), wait exactly this long instead of backoff. */
19
+ retryAfterMs?: number;
20
+ }
21
+ export declare function classifyOutboundError(err: unknown): OutboundErrorClass;
22
+ /** Backoff schedule for the safe class (contract C3). */
23
+ export declare const OUTBOUND_BACKOFFS_MS: readonly [1000, 5000, 25000];
24
+ //# sourceMappingURL=outbound-retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbound-retry.d.ts","sourceRoot":"","sources":["../src/outbound-retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA2BD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAsCtE;AAED,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,8BAA+B,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Classify an outbound send failure (contract C3 outbound).
3
+ *
4
+ * Retry (backoff 1s/5s/25s) ONLY for the provably-undelivered network-error
5
+ * class, because the backend message insert is NOT idempotent:
6
+ * - connection-level errors that fail BEFORE the request reaches the server
7
+ * (ECONNREFUSED, ENOTFOUND, EAI_AGAIN, EHOSTUNREACH, ENETUNREACH,
8
+ * socket-hang-up-before-response)
9
+ * - HTTP 429 (honor Retry-After)
10
+ *
11
+ * Everything else is AMBIGUOUS (the request may have been applied server-side):
12
+ * request timeouts (ECONNABORTED) and 5xx get NO retry, and every other 4xx is
13
+ * a caller error. Ambiguous failures reject to the caller and set a heartbeat
14
+ * lastError, but are never retried or spooled.
15
+ */
16
+ const SAFE_CONNECTION_CODES = new Set([
17
+ "ECONNREFUSED",
18
+ "ENOTFOUND",
19
+ "EAI_AGAIN",
20
+ "EHOSTUNREACH",
21
+ "ENETUNREACH",
22
+ ]);
23
+ function parseRetryAfterMs(headers) {
24
+ if (!headers)
25
+ return undefined;
26
+ const raw = headers["retry-after"] ?? headers["Retry-After"];
27
+ if (raw === undefined || raw === null)
28
+ return undefined;
29
+ const s = String(raw).trim();
30
+ // Numeric seconds form.
31
+ if (/^\d+$/.test(s)) {
32
+ return Number(s) * 1000;
33
+ }
34
+ // HTTP-date form.
35
+ const when = Date.parse(s);
36
+ if (!Number.isNaN(when)) {
37
+ return Math.max(0, when - Date.now());
38
+ }
39
+ return undefined;
40
+ }
41
+ export function classifyOutboundError(err) {
42
+ const e = (err ?? {});
43
+ const status = e.response?.status;
44
+ // HTTP 429, rate limited, safe to retry, honor Retry-After.
45
+ if (status === 429) {
46
+ return {
47
+ retriable: true,
48
+ retryAfterMs: parseRetryAfterMs(e.response?.headers),
49
+ };
50
+ }
51
+ // Any other HTTP response means the request DID reach the server:
52
+ // 5xx and other 4xx are ambiguous (do not retry).
53
+ if (typeof status === "number") {
54
+ return { retriable: false };
55
+ }
56
+ // No HTTP response, connection-level failure.
57
+ const code = e.code;
58
+ if (code && SAFE_CONNECTION_CODES.has(code)) {
59
+ return { retriable: true };
60
+ }
61
+ // socket hang up before any response bytes: retriable. A hang-up that
62
+ // arrives WITH a response is impossible (no status here), so this only
63
+ // fires pre-response.
64
+ const message = e.message ?? "";
65
+ if (code === "ECONNRESET" &&
66
+ /socket hang up/i.test(message)) {
67
+ return { retriable: true };
68
+ }
69
+ // Request timeout (ECONNABORTED) is AMBIGUOUS: the server may have applied
70
+ // the write. No retry.
71
+ return { retriable: false };
72
+ }
73
+ /** Backoff schedule for the safe class (contract C3). */
74
+ export const OUTBOUND_BACKOFFS_MS = [1000, 5000, 25000];
75
+ //# sourceMappingURL=outbound-retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbound-retry.js","sourceRoot":"","sources":["../src/outbound-retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC;IACpC,cAAc;IACd,WAAW;IACX,WAAW;IACX,cAAc;IACd,aAAa;CACd,CAAC,CAAC;AAcH,SAAS,iBAAiB,CAAC,OAA4C;IAGrE,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7D,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACxD,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7B,wBAAwB;IACxB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACpB,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAC1B,CAAC;IACD,kBAAkB;IAClB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAmB,CAAC;IACxC,MAAM,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC;IAElC,4DAA4D;IAC5D,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,OAAO;YACL,SAAS,EAAE,IAAI;YACf,YAAY,EAAE,iBAAiB,CAAC,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC;SACrD,CAAC;IACJ,CAAC;IAED,kEAAkE;IAClE,kDAAkD;IAClD,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;IAED,8CAA8C;IAC9C,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC;IACpB,IAAI,IAAI,IAAI,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5C,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,sEAAsE;IACtE,uEAAuE;IACvE,sBAAsB;IACtB,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC;IAChC,IACE,IAAI,KAAK,YAAY;QACrB,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,EAC/B,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,2EAA2E;IAC3E,uBAAuB;IACvB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAU,CAAC"}