byob-cli 0.2.9 → 0.2.10
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/.codex-plugin/plugin.json +1 -1
- package/README.md +29 -0
- package/bin/byob.js +235 -12
- package/byob-logo.ascii +0 -0
- package/package.json +1 -1
- package/skills/byob_cli/CODEX_CORE.md +14 -0
- package/skills/byob_cli/SKILL.md +24 -0
package/README.md
CHANGED
|
@@ -30,6 +30,14 @@ Refresh synced coding skills later without rerunning OAuth or changing MCP confi
|
|
|
30
30
|
npx -y byob-cli codex sync-skills --project-id <project_id> --token <agent_access_token>
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
For Local Codex billed project editing, the BYOB Agent MCP creates a short-lived SSH-style project connection and returns an `ssh_proxy_command` that uses:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y byob-cli@0.2.10 codex ssh-proxy '{"ssh_endpoint":"wss://..."}'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use `byob-cli@0.2.10` or newer for this route. Older published versions do not contain `codex ssh-proxy`, so they cannot bridge the Local Codex billed connection into the running project container.
|
|
40
|
+
|
|
33
41
|
Or install it globally:
|
|
34
42
|
|
|
35
43
|
```bash
|
|
@@ -193,6 +201,27 @@ npx -y byob-cli codex sync-skills --project-id <project_id> --token <agent_acces
|
|
|
193
201
|
|
|
194
202
|
`codex sync-skills` downloads AIR coding-skill resources and rewrites their local skill names with `byob_` prefixes. It does not run OAuth, does not call `codex mcp add`, and does not change the active MCP server config.
|
|
195
203
|
|
|
204
|
+
### Local Codex Billed SSH Route
|
|
205
|
+
|
|
206
|
+
When a user chooses **Local Codex billed**, prefer the SSH-backed route from the start of the edit session:
|
|
207
|
+
|
|
208
|
+
1. Call `byob_codex_status` for the selected project.
|
|
209
|
+
2. If `Local Codex billed` is available, call `byob_codex_connection_create` with `connection_type: "ssh_gateway"`.
|
|
210
|
+
3. Use the returned SSH connection details through `byob-cli@0.2.10` or newer. Do not print tokens, private keys, or the full proxy command in chat.
|
|
211
|
+
4. Connect with the returned SSH proxy and inspect/edit the remote project filesystem directly.
|
|
212
|
+
5. Fall back to Agent MCP coding tools only when the SSH route is unavailable or the host environment cannot launch the proxy.
|
|
213
|
+
|
|
214
|
+
The proxy command is intentionally a local bridge from OpenSSH stdio to AIR's authenticated WebSocket endpoint:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
BYOB_CODEX_SSH_TOKEN=<connection_token> \
|
|
218
|
+
ssh -i <temporary_key_file> \
|
|
219
|
+
-o ProxyCommand="npx -y byob-cli@0.2.10 codex ssh-proxy '{\"ssh_endpoint\":\"<ssh_endpoint>\"}'" \
|
|
220
|
+
byob@byob-project
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Agents should keep this user-facing: say "Local Codex billed connection" or "SSH-backed project connection", not internal labels such as `ssh_gateway`, unless debugging the connector itself. If `npx -y byob-cli codex ssh-proxy` returns `Unknown command: codex`, the installed/published CLI is too old; upgrade to `byob-cli@0.2.10` or newer instead of retrying the same command.
|
|
224
|
+
|
|
196
225
|
## Multiple Projects
|
|
197
226
|
|
|
198
227
|
BYOB grants can cover more than one approved project. There are two supported install shapes.
|
package/bin/byob.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import { copyFileSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
import tls from "node:tls";
|
|
6
9
|
import { dirname, join } from "node:path";
|
|
7
10
|
import { fileURLToPath } from "node:url";
|
|
8
11
|
import { homedir } from "node:os";
|
|
@@ -38,6 +41,7 @@ Usage:
|
|
|
38
41
|
byob config
|
|
39
42
|
byob codex install
|
|
40
43
|
byob codex sync-skills
|
|
44
|
+
byob codex ssh-proxy [json_args]
|
|
41
45
|
|
|
42
46
|
Options:
|
|
43
47
|
--air-url <url> Default: BYOB_AIR_URL or https://air.api.byob.studio
|
|
@@ -191,12 +195,49 @@ async function mcpRequest(opts, method, params = {}, requestId = Date.now(), pro
|
|
|
191
195
|
});
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
function
|
|
195
|
-
|
|
198
|
+
function publicPreviewUrl(projectId) {
|
|
199
|
+
const id = String(projectId || "").trim();
|
|
200
|
+
return id ? `https://preview.${id}.api.byob.studio` : "";
|
|
196
201
|
}
|
|
197
202
|
|
|
198
|
-
function
|
|
199
|
-
|
|
203
|
+
function sanitizePreviewUrls(value, projectIdHint = "", { sanitizeText = false } = {}) {
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
return value.map((item) => sanitizePreviewUrls(item, projectIdHint, { sanitizeText }));
|
|
206
|
+
}
|
|
207
|
+
if (!value || typeof value !== "object") {
|
|
208
|
+
if (sanitizeText && typeof value === "string") {
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(value);
|
|
211
|
+
return JSON.stringify(sanitizePreviewUrls(parsed, projectIdHint, { sanitizeText }), null, 2);
|
|
212
|
+
} catch {
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const localProjectId = value.project_id || value.id || projectIdHint;
|
|
220
|
+
const sanitized = {};
|
|
221
|
+
for (const [key, item] of Object.entries(value)) {
|
|
222
|
+
if (key === "preview_url") {
|
|
223
|
+
sanitized[key] = publicPreviewUrl(localProjectId) || item;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
sanitized[key] = sanitizePreviewUrls(item, localProjectId, {
|
|
227
|
+
sanitizeText: sanitizeText && key === "text",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return sanitized;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function printJson(payload, projectIdHint = "", sanitizeText = false) {
|
|
234
|
+
const safePayload = sanitizePreviewUrls(payload, projectIdHint, { sanitizeText });
|
|
235
|
+
process.stdout.write(`${JSON.stringify(safePayload, null, 2)}\n`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function printMcpResponse(payload, projectIdHint = "", sanitizeText = false) {
|
|
239
|
+
const safePayload = sanitizePreviewUrls(payload, projectIdHint, { sanitizeText });
|
|
240
|
+
process.stdout.write(`${JSON.stringify(safePayload)}\n`);
|
|
200
241
|
}
|
|
201
242
|
|
|
202
243
|
function byobBanner() {
|
|
@@ -288,7 +329,8 @@ async function serve(opts) {
|
|
|
288
329
|
timeoutSeconds: opts.timeout,
|
|
289
330
|
body: request,
|
|
290
331
|
});
|
|
291
|
-
|
|
332
|
+
const toolName = request?.method === "tools/call" ? request?.params?.name || "" : "";
|
|
333
|
+
printMcpResponse(response, projectId, String(toolName).startsWith("byob_"));
|
|
292
334
|
} catch (error) {
|
|
293
335
|
printMcpResponse({
|
|
294
336
|
jsonrpc: "2.0",
|
|
@@ -645,7 +687,7 @@ async function callTool(opts, toolName, rawArgs) {
|
|
|
645
687
|
name: toolName,
|
|
646
688
|
arguments: toolArgs,
|
|
647
689
|
}, Date.now(), projectId);
|
|
648
|
-
printJson(response);
|
|
690
|
+
printJson(response, projectId, toolName.startsWith("byob_"));
|
|
649
691
|
}
|
|
650
692
|
|
|
651
693
|
async function aliasCall(opts, alias, rawArgs) {
|
|
@@ -668,8 +710,9 @@ async function projects(opts) {
|
|
|
668
710
|
}
|
|
669
711
|
|
|
670
712
|
async function resources(opts) {
|
|
671
|
-
const
|
|
672
|
-
|
|
713
|
+
const projectId = requireProject(opts);
|
|
714
|
+
const response = await mcpRequest(opts, "resources/list", {}, Date.now(), projectId);
|
|
715
|
+
printJson(response, projectId, true);
|
|
673
716
|
}
|
|
674
717
|
|
|
675
718
|
async function context(opts, rawArgs) {
|
|
@@ -679,7 +722,7 @@ async function context(opts, rawArgs) {
|
|
|
679
722
|
name: "byob_current_project_context",
|
|
680
723
|
arguments: { project_id: projectId },
|
|
681
724
|
}, Date.now(), projectId);
|
|
682
|
-
printJson(response);
|
|
725
|
+
printJson(response, projectId, true);
|
|
683
726
|
}
|
|
684
727
|
|
|
685
728
|
async function contextResource(opts, rawArgs) {
|
|
@@ -693,7 +736,180 @@ async function contextResource(opts, rawArgs) {
|
|
|
693
736
|
Date.now(),
|
|
694
737
|
projectId,
|
|
695
738
|
);
|
|
696
|
-
printJson(response);
|
|
739
|
+
printJson(response, projectId, true);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function truthy(value) {
|
|
743
|
+
return ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function websocketFrame(opcode, payload = Buffer.alloc(0)) {
|
|
747
|
+
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
748
|
+
let headerLength = 2;
|
|
749
|
+
if (body.length >= 126 && body.length <= 65535) headerLength += 2;
|
|
750
|
+
else if (body.length > 65535) headerLength += 8;
|
|
751
|
+
const mask = randomBytes(4);
|
|
752
|
+
const frame = Buffer.alloc(headerLength + 4 + body.length);
|
|
753
|
+
frame[0] = 0x80 | opcode;
|
|
754
|
+
if (body.length < 126) {
|
|
755
|
+
frame[1] = 0x80 | body.length;
|
|
756
|
+
} else if (body.length <= 65535) {
|
|
757
|
+
frame[1] = 0x80 | 126;
|
|
758
|
+
frame.writeUInt16BE(body.length, 2);
|
|
759
|
+
} else {
|
|
760
|
+
frame[1] = 0x80 | 127;
|
|
761
|
+
frame.writeBigUInt64BE(BigInt(body.length), 2);
|
|
762
|
+
}
|
|
763
|
+
mask.copy(frame, headerLength);
|
|
764
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
765
|
+
frame[headerLength + 4 + i] = body[i] ^ mask[i % 4];
|
|
766
|
+
}
|
|
767
|
+
return frame;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function parseWebSocketFrames(buffer, onPayload) {
|
|
771
|
+
let offset = 0;
|
|
772
|
+
while (buffer.length - offset >= 2) {
|
|
773
|
+
const first = buffer[offset];
|
|
774
|
+
const second = buffer[offset + 1];
|
|
775
|
+
const opcode = first & 0x0f;
|
|
776
|
+
const masked = Boolean(second & 0x80);
|
|
777
|
+
let length = second & 0x7f;
|
|
778
|
+
let headerLength = 2;
|
|
779
|
+
if (length === 126) {
|
|
780
|
+
if (buffer.length - offset < 4) break;
|
|
781
|
+
length = buffer.readUInt16BE(offset + 2);
|
|
782
|
+
headerLength = 4;
|
|
783
|
+
} else if (length === 127) {
|
|
784
|
+
if (buffer.length - offset < 10) break;
|
|
785
|
+
const bigLength = buffer.readBigUInt64BE(offset + 2);
|
|
786
|
+
if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("WebSocket frame too large.");
|
|
787
|
+
length = Number(bigLength);
|
|
788
|
+
headerLength = 10;
|
|
789
|
+
}
|
|
790
|
+
const maskLength = masked ? 4 : 0;
|
|
791
|
+
if (buffer.length - offset < headerLength + maskLength + length) break;
|
|
792
|
+
const mask = masked ? buffer.subarray(offset + headerLength, offset + headerLength + 4) : null;
|
|
793
|
+
const payloadStart = offset + headerLength + maskLength;
|
|
794
|
+
const payload = Buffer.from(buffer.subarray(payloadStart, payloadStart + length));
|
|
795
|
+
if (mask) {
|
|
796
|
+
for (let i = 0; i < payload.length; i += 1) payload[i] ^= mask[i % 4];
|
|
797
|
+
}
|
|
798
|
+
if (opcode === 0x8) return { remaining: Buffer.alloc(0), closed: true };
|
|
799
|
+
if (opcode === 0x9) onPayload(null, websocketFrame(0xA, payload));
|
|
800
|
+
else if (opcode === 0x1 || opcode === 0x2 || opcode === 0x0) onPayload(payload, null);
|
|
801
|
+
offset = payloadStart + length;
|
|
802
|
+
}
|
|
803
|
+
return { remaining: buffer.subarray(offset), closed: false };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function websocketToStdio(endpoint, token, options = {}) {
|
|
807
|
+
const url = new URL(endpoint);
|
|
808
|
+
const isTls = url.protocol === "wss:";
|
|
809
|
+
if (!["ws:", "wss:"].includes(url.protocol)) {
|
|
810
|
+
throw new Error("SSH endpoint must be a ws:// or wss:// URL.");
|
|
811
|
+
}
|
|
812
|
+
const port = Number(url.port || (isTls ? 443 : 80));
|
|
813
|
+
const tlsCaCert = options.tlsCaCert || process.env.BYOB_CODEX_SSH_CA_CERT || "";
|
|
814
|
+
const tlsInsecure = Boolean(options.tlsInsecure) || truthy(process.env.BYOB_CODEX_SSH_TLS_INSECURE);
|
|
815
|
+
const tlsOptions = {};
|
|
816
|
+
if (isTls) {
|
|
817
|
+
if (tlsCaCert) tlsOptions.ca = readFileSync(tlsCaCert, "utf8");
|
|
818
|
+
if (tlsInsecure) tlsOptions.rejectUnauthorized = false;
|
|
819
|
+
}
|
|
820
|
+
const socket = isTls
|
|
821
|
+
? tls.connect({ host: url.hostname, port, servername: url.hostname, ...tlsOptions })
|
|
822
|
+
: net.connect({ host: url.hostname, port });
|
|
823
|
+
|
|
824
|
+
await new Promise((resolve, reject) => {
|
|
825
|
+
socket.once(isTls ? "secureConnect" : "connect", resolve);
|
|
826
|
+
socket.once("error", reject);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const key = randomBytes(16).toString("base64");
|
|
830
|
+
const path = `${url.pathname || "/"}${url.search || ""}`;
|
|
831
|
+
socket.write(
|
|
832
|
+
[
|
|
833
|
+
`GET ${path} HTTP/1.1`,
|
|
834
|
+
`Host: ${url.host}`,
|
|
835
|
+
"Upgrade: websocket",
|
|
836
|
+
"Connection: Upgrade",
|
|
837
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
838
|
+
"Sec-WebSocket-Version: 13",
|
|
839
|
+
token ? `Authorization: Bearer ${token}` : "",
|
|
840
|
+
"",
|
|
841
|
+
"",
|
|
842
|
+
].filter((line) => line !== null).join("\r\n"),
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
await new Promise((resolve, reject) => {
|
|
846
|
+
let handshake = Buffer.alloc(0);
|
|
847
|
+
function onData(chunk) {
|
|
848
|
+
handshake = Buffer.concat([handshake, chunk]);
|
|
849
|
+
const end = handshake.indexOf("\r\n\r\n");
|
|
850
|
+
if (end === -1) return;
|
|
851
|
+
socket.off("data", onData);
|
|
852
|
+
const headerText = handshake.subarray(0, end).toString("utf8");
|
|
853
|
+
if (!/^HTTP\/1\.1 101\b/i.test(headerText)) {
|
|
854
|
+
reject(new Error(`WebSocket upgrade failed: ${headerText.split(/\r?\n/)[0] || "unknown response"}`));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const acceptHeader = headerText
|
|
858
|
+
.split(/\r?\n/)
|
|
859
|
+
.find((line) => /^sec-websocket-accept:/i.test(line))
|
|
860
|
+
?.split(":")
|
|
861
|
+
.slice(1)
|
|
862
|
+
.join(":")
|
|
863
|
+
.trim();
|
|
864
|
+
const expectedAccept = createHash("sha1")
|
|
865
|
+
.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
|
|
866
|
+
.digest("base64");
|
|
867
|
+
if (acceptHeader !== expectedAccept) {
|
|
868
|
+
reject(new Error("WebSocket upgrade failed: invalid Sec-WebSocket-Accept header"));
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const leftover = handshake.subarray(end + 4);
|
|
872
|
+
if (leftover.length) socket.emit("byob-leftover", leftover);
|
|
873
|
+
resolve();
|
|
874
|
+
}
|
|
875
|
+
socket.on("data", onData);
|
|
876
|
+
socket.once("error", reject);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
let frameBuffer = Buffer.alloc(0);
|
|
880
|
+
const handleChunk = (chunk) => {
|
|
881
|
+
frameBuffer = Buffer.concat([frameBuffer, chunk]);
|
|
882
|
+
const parsed = parseWebSocketFrames(frameBuffer, (payload, controlFrame) => {
|
|
883
|
+
if (controlFrame) socket.write(controlFrame);
|
|
884
|
+
if (payload) process.stdout.write(payload);
|
|
885
|
+
});
|
|
886
|
+
frameBuffer = parsed.remaining;
|
|
887
|
+
if (parsed.closed) process.exit(0);
|
|
888
|
+
};
|
|
889
|
+
socket.on("byob-leftover", handleChunk);
|
|
890
|
+
socket.on("data", handleChunk);
|
|
891
|
+
socket.on("close", () => process.exit(0));
|
|
892
|
+
socket.on("error", (error) => {
|
|
893
|
+
process.stderr.write(`${error.message}\n`);
|
|
894
|
+
process.exit(1);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
process.stdin.on("data", (chunk) => socket.write(websocketFrame(0x2, chunk)));
|
|
898
|
+
process.stdin.on("end", () => socket.write(websocketFrame(0x8)));
|
|
899
|
+
process.stdin.resume();
|
|
900
|
+
await new Promise((resolve) => socket.once("close", resolve));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
async function codexSshProxy(_opts, rawArgs) {
|
|
904
|
+
const args = parseJsonArg(rawArgs);
|
|
905
|
+
const endpoint = args.ssh_endpoint || process.env.BYOB_CODEX_SSH_ENDPOINT || "";
|
|
906
|
+
const tokenEnvVar = args.token_env_var || "BYOB_CODEX_SSH_TOKEN";
|
|
907
|
+
const token = args.token || process.env[tokenEnvVar] || process.env.BYOB_CODEX_SSH_TOKEN || "";
|
|
908
|
+
const tlsCaCert = args.tls_ca_cert || args.ca_cert || process.env.BYOB_CODEX_SSH_CA_CERT || "";
|
|
909
|
+
const tlsInsecure = truthy(args.tls_insecure) || truthy(args.insecure_tls);
|
|
910
|
+
if (!endpoint) throw new Error("Missing SSH endpoint. Pass {\"ssh_endpoint\":\"wss://...\"} or set BYOB_CODEX_SSH_ENDPOINT.");
|
|
911
|
+
if (!token) throw new Error(`Missing SSH token. Set ${tokenEnvVar}.`);
|
|
912
|
+
await websocketToStdio(endpoint, token, { tlsCaCert, tlsInsecure });
|
|
697
913
|
}
|
|
698
914
|
|
|
699
915
|
async function main() {
|
|
@@ -708,6 +924,7 @@ async function main() {
|
|
|
708
924
|
if (command === "mcp") return serve(opts);
|
|
709
925
|
if (command === "codex" && first === "install") return codexInstall(opts);
|
|
710
926
|
if (command === "codex" && first === "sync-skills") return codexSyncSkills(opts);
|
|
927
|
+
if (command === "codex" && first === "ssh-proxy") return codexSshProxy(opts, second);
|
|
711
928
|
if (command === "config") return config(opts);
|
|
712
929
|
if (command === "device-code") return deviceCode(opts);
|
|
713
930
|
if (command === "token") return token(opts, first);
|
|
@@ -716,8 +933,14 @@ async function main() {
|
|
|
716
933
|
if (command === "context-resource") return contextResource(opts, first);
|
|
717
934
|
if (command === "resources") return resources(opts);
|
|
718
935
|
if (command === "projects") return projects(opts);
|
|
719
|
-
if (command === "initialize")
|
|
720
|
-
|
|
936
|
+
if (command === "initialize") {
|
|
937
|
+
const projectId = requireProject(opts);
|
|
938
|
+
return printJson(await mcpRequest(opts, "initialize", {}, Date.now(), projectId), projectId, true);
|
|
939
|
+
}
|
|
940
|
+
if (command === "tools") {
|
|
941
|
+
const projectId = requireProject(opts);
|
|
942
|
+
return printJson(await mcpRequest(opts, "tools/list", {}, Date.now(), projectId), projectId, true);
|
|
943
|
+
}
|
|
721
944
|
if (command === "call") return callTool(opts, first, second);
|
|
722
945
|
if (["status", "start", "deploy", "billing", "credits", "payment-url", "env-status", "grants", "dns"].includes(command)) {
|
|
723
946
|
return aliasCall(opts, command, first);
|
package/byob-logo.ascii
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -65,6 +65,20 @@ Use coding/container tools for:
|
|
|
65
65
|
|
|
66
66
|
Do not add BYOB platform helper tools into user containers. Do not ask for BYOB internal tokens, editor tokens, preview tokens, Git tokens, Cloudflare tokens, service-role keys, or raw MCP credentials.
|
|
67
67
|
|
|
68
|
+
## Local Codex Billed Route
|
|
69
|
+
|
|
70
|
+
When the user selects **Local Codex billed**, prefer the SSH-backed project connection before trying lower-fidelity Agent MCP workarounds. The expected path is:
|
|
71
|
+
|
|
72
|
+
1. Establish active project context.
|
|
73
|
+
2. Ensure the project is started and remote editing is available.
|
|
74
|
+
3. Call `byob_codex_status`.
|
|
75
|
+
4. Create a Local Codex billed connection with `byob_codex_connection_create`.
|
|
76
|
+
5. Use the returned SSH connection through `byob-cli@0.2.10` or newer's `codex ssh-proxy`.
|
|
77
|
+
|
|
78
|
+
This route should let Codex inspect and edit the remote project filesystem directly, including protected files such as `src/app.html` when the task requires reading them. Keep all returned tokens, private keys, and complete proxy commands out of chat and logs. Use user-facing language like "Local Codex billed connection" or "SSH-backed project connection"; avoid internal enum names unless debugging BYOB itself.
|
|
79
|
+
|
|
80
|
+
If `npx -y byob-cli codex ssh-proxy` fails with `Unknown command: codex`, treat it as a BYOB CLI version mismatch. The SSH proxy command requires `byob-cli@0.2.10` or newer. Do not continue by repeatedly trying Agent MCP allowlisted command facades for file reads; those facades are designed for build/check/lint/test commands and will reject arbitrary shell/file inspection.
|
|
81
|
+
|
|
68
82
|
## Skill Routing
|
|
69
83
|
|
|
70
84
|
`byob_cli` is the platform/core skill. Load separate synced skills only when the task calls for them. Do not load every skill by default.
|
package/skills/byob_cli/SKILL.md
CHANGED
|
@@ -53,6 +53,16 @@ Refresh downloaded coding skills later without rerunning OAuth or changing MCP c
|
|
|
53
53
|
npx -y byob-cli codex sync-skills --project-id <project_id> --token <agent_access_token>
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
For **Local Codex billed** project editing, prefer the SSH-backed project connection immediately instead of first trying to simulate shell/file access through Agent MCP tools:
|
|
57
|
+
|
|
58
|
+
1. Establish project context with `byob_current_project_context`.
|
|
59
|
+
2. Call `byob_codex_status` and confirm the Local Codex billed option is available.
|
|
60
|
+
3. Call `byob_codex_connection_create` with the Local Codex billed connection type.
|
|
61
|
+
4. Use the returned SSH proxy connection through `byob-cli@0.2.10` or newer to inspect and edit files in the remote project container.
|
|
62
|
+
5. Do not print tokens, private keys, or full proxy commands in chat.
|
|
63
|
+
|
|
64
|
+
If `npx -y byob-cli codex ssh-proxy` reports `Unknown command: codex`, the local/published CLI is too old for Local Codex billed SSH. Tell the user to use `byob-cli@0.2.10` or newer; do not keep retrying Agent MCP command facades for ordinary file reads. Use Agent MCP project tools as a fallback only when the SSH route is unavailable or the host cannot launch the proxy.
|
|
65
|
+
|
|
56
66
|
Equivalent raw PRIM flow:
|
|
57
67
|
|
|
58
68
|
1. Request a device code from PRIM:
|
|
@@ -272,6 +282,20 @@ Coding tools are proxied by AIR into the private project container MCP:
|
|
|
272
282
|
|
|
273
283
|
Do not assume `byob_*` tools exist inside the user container. They are platform tools and belong to Agent MCP only.
|
|
274
284
|
|
|
285
|
+
## Local Codex Billed Editing
|
|
286
|
+
|
|
287
|
+
When the user chooses Local Codex billed, the expected editing path is a short-lived SSH-backed connection into the remote BYOB project container. This gives Codex normal filesystem semantics for reading `src/app.html`, searching source, editing files, and running project-local shell commands through the local Codex account billing path.
|
|
288
|
+
|
|
289
|
+
Use this flow:
|
|
290
|
+
|
|
291
|
+
1. Confirm the project is started and `connectivity.can_remote_edit` is true.
|
|
292
|
+
2. Call `byob_codex_status`.
|
|
293
|
+
3. If Local Codex billed is available, call `byob_codex_connection_create` for that mode.
|
|
294
|
+
4. Use the returned SSH connection details with `byob-cli@0.2.10` or newer's `codex ssh-proxy`.
|
|
295
|
+
5. Keep credentials out of chat, logs, screenshots, and command transcripts.
|
|
296
|
+
|
|
297
|
+
If the SSH proxy cannot be launched because the installed CLI lacks `codex ssh-proxy`, report that as a CLI version mismatch and ask for/choose `byob-cli@0.2.10` or newer. Avoid frustrating loops where Codex repeatedly tries Agent MCP allowlisted commands to read files; those command facades are intentionally limited to build/check/lint/test and are not a replacement for the SSH route.
|
|
298
|
+
|
|
275
299
|
## Project Scope
|
|
276
300
|
|
|
277
301
|
The user approves specific projects during browser approval. The agent token can only access those project ids.
|