byob-cli 0.2.9 → 0.2.11
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 opens the project container as the active Codex workspace. It is powered by `codex ssh-proxy`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y byob-cli@latest codex ssh-proxy '{"ssh_endpoint":"wss://..."}'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use `byob-cli@latest` for Local Codex billed.
|
|
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
|
|
205
|
+
|
|
206
|
+
When a user chooses **Local Codex billed**, open the BYOB project as the active Codex workspace 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. Hand the returned workspace metadata to the Codex host so `/home/workspace/app_data` becomes the active workspace.
|
|
211
|
+
4. Keep tokens, private keys, and complete proxy commands out of chat.
|
|
212
|
+
5. Use Codex's normal file, shell, and git workflow inside that active workspace.
|
|
213
|
+
|
|
214
|
+
The underlying proxy command bridges 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@latest codex ssh-proxy '{\"ssh_endpoint\":\"<ssh_endpoint>\"}'" \
|
|
220
|
+
byob@byob-project
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Agents and hosts should keep this user-facing: say "Local Codex billed", not internal labels such as `ssh_gateway`, unless debugging the connector itself.
|
|
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**, open the BYOB project as the active Codex workspace 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 Local Codex billed workspace details through `byob-cli@latest`'s `codex ssh-proxy`.
|
|
77
|
+
|
|
78
|
+
Local Codex billed 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"; avoid internal enum names unless debugging BYOB itself.
|
|
79
|
+
|
|
80
|
+
After the workspace is open, use Codex's normal file, shell, git, and verification workflow inside the remote project.
|
|
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, open the BYOB project as the active Codex workspace immediately instead of 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 Local Codex billed workspace metadata through `byob-cli@latest` so Codex edits the remote project container as its active workspace.
|
|
62
|
+
5. Do not print tokens, private keys, or full proxy commands in chat.
|
|
63
|
+
|
|
64
|
+
After the workspace is open, use Codex's normal file, shell, git, and verification workflow inside the remote project.
|
|
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 that the remote BYOB project container becomes the active Codex workspace. 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 Local Codex billed workspace details with `byob-cli@latest`'s `codex ssh-proxy`.
|
|
295
|
+
5. Keep credentials out of chat, logs, screenshots, and command transcripts.
|
|
296
|
+
|
|
297
|
+
After the workspace is open, use Codex's normal file, shell, git, and verification workflow inside the remote project.
|
|
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.
|