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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/adapter.d.ts +96 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +677 -0
- package/dist/adapter.js.map +1 -0
- package/dist/agent-hints.d.ts +20 -0
- package/dist/agent-hints.d.ts.map +1 -0
- package/dist/agent-hints.js +92 -0
- package/dist/agent-hints.js.map +1 -0
- package/dist/agent-identity.d.ts +36 -0
- package/dist/agent-identity.d.ts.map +1 -0
- package/dist/agent-identity.js +42 -0
- package/dist/agent-identity.js.map +1 -0
- package/dist/attachment-bridge.d.ts +64 -0
- package/dist/attachment-bridge.d.ts.map +1 -0
- package/dist/attachment-bridge.js +216 -0
- package/dist/attachment-bridge.js.map +1 -0
- package/dist/auth-mode.d.ts +34 -0
- package/dist/auth-mode.d.ts.map +1 -0
- package/dist/auth-mode.js +48 -0
- package/dist/auth-mode.js.map +1 -0
- package/dist/bgos-api.d.ts +191 -0
- package/dist/bgos-api.d.ts.map +1 -0
- package/dist/bgos-api.js +225 -0
- package/dist/bgos-api.js.map +1 -0
- package/dist/bgos-ws.d.ts +94 -0
- package/dist/bgos-ws.d.ts.map +1 -0
- package/dist/bgos-ws.js +310 -0
- package/dist/bgos-ws.js.map +1 -0
- package/dist/capabilities.d.ts +28 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +44 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/catalog-sync.d.ts +26 -0
- package/dist/catalog-sync.d.ts.map +1 -0
- package/dist/catalog-sync.js +24 -0
- package/dist/catalog-sync.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +152 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-host.d.ts +44 -0
- package/dist/codex-host.d.ts.map +1 -0
- package/dist/codex-host.js +142 -0
- package/dist/codex-host.js.map +1 -0
- package/dist/commands-sync.d.ts +24 -0
- package/dist/commands-sync.d.ts.map +1 -0
- package/dist/commands-sync.js +43 -0
- package/dist/commands-sync.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/default-commands.d.ts +27 -0
- package/dist/default-commands.d.ts.map +1 -0
- package/dist/default-commands.js +64 -0
- package/dist/default-commands.js.map +1 -0
- package/dist/event-mapper.d.ts +43 -0
- package/dist/event-mapper.d.ts.map +1 -0
- package/dist/event-mapper.js +127 -0
- package/dist/event-mapper.js.map +1 -0
- package/dist/heartbeat.d.ts +75 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +186 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/inbound-handler.d.ts +167 -0
- package/dist/inbound-handler.d.ts.map +1 -0
- package/dist/inbound-handler.js +282 -0
- package/dist/inbound-handler.js.map +1 -0
- package/dist/inbound-input.d.ts +17 -0
- package/dist/inbound-input.d.ts.map +1 -0
- package/dist/inbound-input.js +25 -0
- package/dist/inbound-input.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/install-service.d.ts +8 -0
- package/dist/install-service.d.ts.map +1 -0
- package/dist/install-service.js +66 -0
- package/dist/install-service.js.map +1 -0
- package/dist/last-id-store.d.ts +33 -0
- package/dist/last-id-store.d.ts.map +1 -0
- package/dist/last-id-store.js +101 -0
- package/dist/last-id-store.js.map +1 -0
- package/dist/load-config.d.ts +30 -0
- package/dist/load-config.d.ts.map +1 -0
- package/dist/load-config.js +86 -0
- package/dist/load-config.js.map +1 -0
- package/dist/media-classify.d.ts +37 -0
- package/dist/media-classify.d.ts.map +1 -0
- package/dist/media-classify.js +108 -0
- package/dist/media-classify.js.map +1 -0
- package/dist/media-guard.d.ts +13 -0
- package/dist/media-guard.d.ts.map +1 -0
- package/dist/media-guard.js +159 -0
- package/dist/media-guard.js.map +1 -0
- package/dist/outbound-retry.d.ts +24 -0
- package/dist/outbound-retry.d.ts.map +1 -0
- package/dist/outbound-retry.js +75 -0
- package/dist/outbound-retry.js.map +1 -0
- package/dist/outbound.d.ts +223 -0
- package/dist/outbound.d.ts.map +1 -0
- package/dist/outbound.js +366 -0
- package/dist/outbound.js.map +1 -0
- package/dist/outbox.d.ts +16 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +88 -0
- package/dist/outbox.js.map +1 -0
- package/dist/pair-cli.d.ts +44 -0
- package/dist/pair-cli.d.ts.map +1 -0
- package/dist/pair-cli.js +182 -0
- package/dist/pair-cli.js.map +1 -0
- package/dist/pending-unknown-store.d.ts +26 -0
- package/dist/pending-unknown-store.d.ts.map +1 -0
- package/dist/pending-unknown-store.js +135 -0
- package/dist/pending-unknown-store.js.map +1 -0
- package/dist/processed-ids.d.ts +36 -0
- package/dist/processed-ids.d.ts.map +1 -0
- package/dist/processed-ids.js +55 -0
- package/dist/processed-ids.js.map +1 -0
- package/dist/reply-markers.d.ts +42 -0
- package/dist/reply-markers.d.ts.map +1 -0
- package/dist/reply-markers.js +124 -0
- package/dist/reply-markers.js.map +1 -0
- package/dist/setup/supervisor.d.ts +16 -0
- package/dist/setup/supervisor.d.ts.map +1 -0
- package/dist/setup/supervisor.js +61 -0
- package/dist/setup/supervisor.js.map +1 -0
- package/dist/thread-map.d.ts +12 -0
- package/dist/thread-map.d.ts.map +1 -0
- package/dist/thread-map.js +72 -0
- package/dist/thread-map.js.map +1 -0
- package/dist/tool-progress.d.ts +103 -0
- package/dist/tool-progress.d.ts.map +1 -0
- package/dist/tool-progress.js +216 -0
- package/dist/tool-progress.js.map +1 -0
- package/dist/types.d.ts +244 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +28 -0
- package/dist/version.js.map +1 -0
- package/dist/voice-rpc.d.ts +37 -0
- package/dist/voice-rpc.d.ts.map +1 -0
- package/dist/voice-rpc.js +44 -0
- package/dist/voice-rpc.js.map +1 -0
- 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"}
|