Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@
"@corbits/url-path": "workspace:*",
"@corbits/workflows": "workspace:*",
"@intx/types": "workspace:*",
"@isomorphic-git/lightning-fs": "4.7.0",
"@phosphor-icons/react": "^2.1.10",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-query": "catalog:",
"arktype": "catalog:",
"buffer": "^6.0.3",
"isomorphic-git": "catalog:",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
Expand Down
118 changes: 118 additions & 0 deletions apps/web/src/git-push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Pushes a small flat file tree into a hub asset repo from the browser.
// isomorphic-git builds the objects and the pack; the receive-pack wire
// exchange is spoken directly because the hub answers `report-status`
// as raw pkt-lines, which isomorphic-git's own push cannot read.
import LightningFS from "@isomorphic-git/lightning-fs";
import { Buffer } from "buffer";
import git from "isomorphic-git";

// isomorphic-git reads the Node `Buffer` global; browsers do not ship one.
globalThis.Buffer ??= Buffer;

export class GitPushError extends Error {}

const COMMIT_AUTHOR = { name: "Workbench", email: "workbench@corbits.dev" };
const MAIN_REF = "refs/heads/main";
const ZERO_OID = "0".repeat(40);

function pktLine(text: string): Uint8Array {
const payload = new TextEncoder().encode(text);
const header = (payload.length + 4).toString(16).padStart(4, "0");
return new Uint8Array([...new TextEncoder().encode(header), ...payload]);
}

function readPktLines(body: Uint8Array): string[] {
const decoder = new TextDecoder();
const lines: string[] = [];
let offset = 0;
while (offset + 4 <= body.length) {
const length = parseInt(decoder.decode(body.subarray(offset, offset + 4)), 16);
offset += 4;
if (Number.isNaN(length)) throw new GitPushError("malformed pkt-line header from the hub");
if (length === 0) continue;
lines.push(decoder.decode(body.subarray(offset, offset + length - 4)));
offset += length - 4;
}
return lines;
}

async function advertisedMainSha(url: string, token: string): Promise<string> {
const response = await fetch(`${url}/info/refs?service=git-receive-pack`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
throw new GitPushError(`ref advertisement failed: HTTP ${response.status}`);
}
const lines = readPktLines(new Uint8Array(await response.arrayBuffer()));
for (const line of lines) {
const [sha, rest] = line.split(" ", 2);
const ref = rest?.split("\0")[0]?.trim();
if (sha !== undefined && ref === MAIN_REF) return sha;
}
return ZERO_OID;
}

/** Commits `tree` on top of the asset's current `main` and pushes it.
* Returns the new commit sha. */
export async function pushSourceTree(args: {
url: string;
token: string;
tree: Readonly<Record<string, string>>;
message: string;
}): Promise<string> {
const fs = new LightningFS(`workbench-push-${crypto.randomUUID()}`, { wipe: true });
const dir = "/repo";
await fs.promises.mkdir(dir);
await git.init({ fs, dir, defaultBranch: "main" });
for (const [filepath, contents] of Object.entries(args.tree)) {
if (filepath.includes("/")) {
throw new GitPushError(`pushSourceTree writes a flat tree; got ${JSON.stringify(filepath)}`);
}
await fs.promises.writeFile(`${dir}/${filepath}`, contents, "utf8");
await git.add({ fs, dir, filepath });
}
const oldSha = await advertisedMainSha(args.url, args.token);
const sha = await git.commit({
fs,
dir,
message: args.message,
author: COMMIT_AUTHOR,
parent: oldSha === ZERO_OID ? [] : [oldSha],
});
const { commit } = await git.readCommit({ fs, dir, oid: sha });
const { tree } = await git.readTree({ fs, dir, oid: commit.tree });
const { packfile } = await git.packObjects({
fs,
dir,
oids: [sha, commit.tree, ...tree.map((entry) => entry.oid)],
});
if (packfile === undefined) throw new GitPushError("packObjects returned no packfile");

const command = pktLine(`${oldSha} ${sha} ${MAIN_REF}\0report-status\n`);
const body = new Uint8Array(command.length + 4 + packfile.length);
body.set(command, 0);
body.set(new TextEncoder().encode("0000"), command.length);
body.set(packfile, command.length + 4);
const response = await fetch(`${args.url}/git-receive-pack`, {
method: "POST",
headers: {
Authorization: `Bearer ${args.token}`,
"content-type": "application/x-git-receive-pack-request",
},
body,
});
if (!response.ok) {
throw new GitPushError(`git push failed: HTTP ${response.status}`);
}
const report = readPktLines(new Uint8Array(await response.arrayBuffer()));
const refusal = report.find(
(line) => line.startsWith("ng ") || (line.startsWith("unpack ") && line.trim() !== "unpack ok"),
);
if (refusal !== undefined) {
throw new GitPushError(`git push was refused: ${refusal.trim()}`);
}
if (!report.some((line) => line.trim() === `ok ${MAIN_REF}`)) {
throw new GitPushError(`git push reported no result for ${MAIN_REF}`);
}
return sha;
}
41 changes: 20 additions & 21 deletions apps/web/src/myra-deploy.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import { describe, expect, test } from "bun:test";

import { buildMyraDeployInput, MyraDeployError, packTarball } from "./myra-deploy";
import { buildMyraDefinitionJson, buildMyraDeployInput, MyraDeployError } from "./myra-deploy";
import { MYRA_SOURCE_CONFIG } from "./myra-source";

describe("buildMyraDefinitionJson", () => {
test("declares the offering chain's sources so the probe approves them", () => {
const definition = buildMyraDefinitionJson("assistant@ada.example", [
{ provider: "openai-compatible", model: "qwen2.5:7b" },
{ provider: "anthropic", model: "claude-sonnet-5" },
]) as { steps: Record<string, { agent: { inference: { sources: unknown } } }> };

expect(definition.steps["assistant"]?.agent.inference.sources).toEqual([
{ provider: "openai-compatible", model: "qwen2.5:7b" },
{ provider: "anthropic", model: "claude-sonnet-5" },
]);
});
});

describe("buildMyraDeployInput", () => {
test("maps a published asset and the operator's offering pick to a WorkflowDeployInput", () => {
test("maps the pushed commit and the operator's offering pick to a source-tree deploy", () => {
const input = buildMyraDeployInput({
assetId: "ast_123",
commitSha: "abc123",
sourceOfferingIds: ["off_1", "off_2"],
defaultSourceOfferingId: "off_2",
});
Expand All @@ -15,19 +30,19 @@ describe("buildMyraDeployInput", () => {
source: {
kind: "asset",
assetId: "ast_123",
package: { format: "tarball" },
package: { format: "source", commitSha: "abc123" },
},
entry: MYRA_SOURCE_CONFIG.entryPath,
sourceOfferingIds: ["off_1", "off_2"],
defaultSourceOfferingId: "off_2",
pin: `${MYRA_SOURCE_CONFIG.packageName}@${MYRA_SOURCE_CONFIG.packageVersion}`,
});
});

test("rejects an empty offering list", () => {
expect(() =>
buildMyraDeployInput({
assetId: "ast_123",
commitSha: "abc123",
sourceOfferingIds: [],
defaultSourceOfferingId: "off_1",
}),
Expand All @@ -38,26 +53,10 @@ describe("buildMyraDeployInput", () => {
expect(() =>
buildMyraDeployInput({
assetId: "ast_123",
commitSha: "abc123",
sourceOfferingIds: ["off_1"],
defaultSourceOfferingId: "off_2",
}),
).toThrow(MyraDeployError);
});
});

describe("packTarball", () => {
test("writes USTAR headers whose checksum verifies and whose typeflag survives", async () => {
const tarball = await packTarball({ "package.json": "{}" });
const tar = new Uint8Array(
await new Response(
new Blob([tarball as Uint8Array<ArrayBuffer>]).stream().pipeThrough(new DecompressionStream("gzip")),
).arrayBuffer(),
);
const header = tar.slice(0, 512);
let expected = 0;
for (let i = 0; i < 512; i++) expected += i >= 148 && i < 156 ? 0x20 : header[i]!;
const stored = parseInt(new TextDecoder().decode(header.slice(148, 154)), 8);
expect(stored).toBe(expected);
expect(String.fromCharCode(header[156]!)).toBe("0");
});
});
Loading
Loading