feat: integrate mobile work chat and Gitea delivery
This commit is contained in:
85
packages/primitives/src/agent-os-git-software.test.ts
Normal file
85
packages/primitives/src/agent-os-git-software.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import git from "@agentos-software/git";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getExecutableGitSoftware,
|
||||
makeGitExecutablesRunnable,
|
||||
} from "./agent-os-git-software";
|
||||
|
||||
const TAR_BLOCK_SIZE = 512;
|
||||
const READ_ONLY_EXECUTABLE_MODE = Buffer.from([0xa4, 0x81, 0x00, 0x00]);
|
||||
const RUNNABLE_EXECUTABLE_MODE = Buffer.from([0xed, 0x81, 0x00, 0x00]);
|
||||
|
||||
const readTarModes = (packageBytes: Buffer): Map<string, number> => {
|
||||
const tarOffset =
|
||||
16 + packageBytes.readUInt32LE(8) + packageBytes.readUInt32LE(12);
|
||||
const modes = new Map<string, number>();
|
||||
let offset = tarOffset;
|
||||
|
||||
while (
|
||||
offset + TAR_BLOCK_SIZE <= packageBytes.length &&
|
||||
packageBytes
|
||||
.subarray(offset, offset + TAR_BLOCK_SIZE)
|
||||
.some((byte) => byte !== 0)
|
||||
) {
|
||||
const [name = ""] = packageBytes
|
||||
.subarray(offset, offset + 100)
|
||||
.toString("utf-8")
|
||||
.split("\0");
|
||||
const mode = Number.parseInt(
|
||||
packageBytes
|
||||
.subarray(offset + 100, offset + 108)
|
||||
.toString("ascii")
|
||||
.replaceAll("\0", "")
|
||||
.trim(),
|
||||
8
|
||||
);
|
||||
const size = Number.parseInt(
|
||||
packageBytes
|
||||
.subarray(offset + 124, offset + 136)
|
||||
.toString("ascii")
|
||||
.replaceAll("\0", "")
|
||||
.trim() || "0",
|
||||
8
|
||||
);
|
||||
|
||||
modes.set(name, mode);
|
||||
offset +=
|
||||
TAR_BLOCK_SIZE + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
return modes;
|
||||
};
|
||||
|
||||
describe("AgentOS Git software", () => {
|
||||
it("marks the packaged Git commands executable", () => {
|
||||
const repaired = makeGitExecutablesRunnable(readFileSync(git.packagePath));
|
||||
const modes = readTarModes(repaired);
|
||||
|
||||
expect(modes.get("./bin/git")).toBe(0o755);
|
||||
expect(modes.get("./bin/git-remote-http")).toBe(0o755);
|
||||
expect(modes.get("./bin/git-remote-https")).toBe(0o755);
|
||||
expect(repaired.includes(Buffer.from("0.3.0-zp.1", "ascii"))).toBe(true);
|
||||
|
||||
const mountIndexOffset = 16 + repaired.readUInt32LE(8);
|
||||
const tarOffset = mountIndexOffset + repaired.readUInt32LE(12);
|
||||
const mountIndex = repaired.subarray(mountIndexOffset, tarOffset);
|
||||
expect(mountIndex.includes(READ_ONLY_EXECUTABLE_MODE)).toBe(false);
|
||||
expect(
|
||||
mountIndex.toString("hex").split(RUNNABLE_EXECUTABLE_MODE.toString("hex"))
|
||||
.length - 1
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
it("writes a stable repaired package for the AgentOS registry", () => {
|
||||
const first = getExecutableGitSoftware();
|
||||
const second = getExecutableGitSoftware();
|
||||
|
||||
expect(second).toEqual(first);
|
||||
expect(readTarModes(readFileSync(first.packagePath)).get("./bin/git")).toBe(
|
||||
0o755
|
||||
);
|
||||
});
|
||||
});
|
||||
194
packages/primitives/src/agent-os-git-software.ts
Normal file
194
packages/primitives/src/agent-os-git-software.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import git from "@agentos-software/git";
|
||||
|
||||
const AOS_PACKAGE_MAGIC = Buffer.from([0x89, 0x41, 0x4f, 0x53]);
|
||||
const TAR_BLOCK_SIZE = 512;
|
||||
const TAR_CHECKSUM_OFFSET = 148;
|
||||
const TAR_CHECKSUM_SIZE = 8;
|
||||
const TAR_MODE_OFFSET = 100;
|
||||
const TAR_MODE_SIZE = 8;
|
||||
const TAR_NAME_SIZE = 100;
|
||||
const TAR_SIZE_OFFSET = 124;
|
||||
const TAR_SIZE_SIZE = 12;
|
||||
const GIT_EXECUTABLES = new Set([
|
||||
"./bin/git",
|
||||
"./bin/git-remote-http",
|
||||
"./bin/git-remote-https",
|
||||
]);
|
||||
const UPSTREAM_GIT_PACKAGE_VERSION = Buffer.from("0.3.0-rc.2", "ascii");
|
||||
const REPAIRED_GIT_PACKAGE_VERSION = Buffer.from("0.3.0-zp.1", "ascii");
|
||||
const READ_ONLY_EXECUTABLE_MODE = Buffer.from([0xa4, 0x81, 0x00, 0x00]);
|
||||
const RUNNABLE_EXECUTABLE_MODE = Buffer.from([0xed, 0x81, 0x00, 0x00]);
|
||||
|
||||
const parseTarOctal = (
|
||||
packageBytes: Buffer,
|
||||
offset: number,
|
||||
length: number
|
||||
): number => {
|
||||
const value = packageBytes
|
||||
.subarray(offset, offset + length)
|
||||
.toString("ascii")
|
||||
.replaceAll("\0", "")
|
||||
.trim();
|
||||
|
||||
return value.length === 0 ? 0 : Number.parseInt(value, 8);
|
||||
};
|
||||
|
||||
const readTarName = (packageBytes: Buffer, headerOffset: number): string => {
|
||||
const [name = ""] = packageBytes
|
||||
.subarray(headerOffset, headerOffset + TAR_NAME_SIZE)
|
||||
.toString("utf-8")
|
||||
.split("\0");
|
||||
return name;
|
||||
};
|
||||
|
||||
const isEmptyTarHeader = (
|
||||
packageBytes: Buffer,
|
||||
headerOffset: number
|
||||
): boolean =>
|
||||
packageBytes
|
||||
.subarray(headerOffset, headerOffset + TAR_BLOCK_SIZE)
|
||||
.every((byte) => byte === 0);
|
||||
|
||||
const writeTarChecksum = (packageBytes: Buffer, headerOffset: number): void => {
|
||||
packageBytes.fill(
|
||||
0x20,
|
||||
headerOffset + TAR_CHECKSUM_OFFSET,
|
||||
headerOffset + TAR_CHECKSUM_OFFSET + TAR_CHECKSUM_SIZE
|
||||
);
|
||||
|
||||
let checksum = 0;
|
||||
for (
|
||||
let index = headerOffset;
|
||||
index < headerOffset + TAR_BLOCK_SIZE;
|
||||
index += 1
|
||||
) {
|
||||
checksum += packageBytes[index] ?? 0;
|
||||
}
|
||||
|
||||
const encodedChecksum = `${checksum.toString(8).padStart(6, "0")}\0 `;
|
||||
packageBytes.write(
|
||||
encodedChecksum,
|
||||
headerOffset + TAR_CHECKSUM_OFFSET,
|
||||
TAR_CHECKSUM_SIZE,
|
||||
"ascii"
|
||||
);
|
||||
};
|
||||
|
||||
const getTarOffset = (packageBytes: Buffer): number => {
|
||||
if (
|
||||
packageBytes.length < 16 ||
|
||||
!packageBytes
|
||||
.subarray(0, AOS_PACKAGE_MAGIC.length)
|
||||
.equals(AOS_PACKAGE_MAGIC)
|
||||
) {
|
||||
throw new Error("Invalid AgentOS package header");
|
||||
}
|
||||
|
||||
return 16 + packageBytes.readUInt32LE(8) + packageBytes.readUInt32LE(12);
|
||||
};
|
||||
|
||||
export const makeGitExecutablesRunnable = (source: Uint8Array): Buffer => {
|
||||
const packageBytes = Buffer.from(source);
|
||||
const mountIndexOffset = 16 + packageBytes.readUInt32LE(8);
|
||||
const tarOffset = getTarOffset(packageBytes);
|
||||
const versionOffset = packageBytes.indexOf(
|
||||
UPSTREAM_GIT_PACKAGE_VERSION,
|
||||
0,
|
||||
"ascii"
|
||||
);
|
||||
if (versionOffset === -1 || versionOffset >= tarOffset) {
|
||||
throw new Error("Expected AgentOS Git package version was not found");
|
||||
}
|
||||
REPAIRED_GIT_PACKAGE_VERSION.copy(packageBytes, versionOffset);
|
||||
|
||||
let mountModeOffset = mountIndexOffset;
|
||||
let repairedMountEntries = 0;
|
||||
while (mountModeOffset < tarOffset) {
|
||||
mountModeOffset = packageBytes.indexOf(
|
||||
READ_ONLY_EXECUTABLE_MODE,
|
||||
mountModeOffset
|
||||
);
|
||||
if (mountModeOffset < 0 || mountModeOffset >= tarOffset) {
|
||||
break;
|
||||
}
|
||||
|
||||
RUNNABLE_EXECUTABLE_MODE.copy(packageBytes, mountModeOffset);
|
||||
repairedMountEntries += 1;
|
||||
mountModeOffset += RUNNABLE_EXECUTABLE_MODE.length;
|
||||
}
|
||||
|
||||
if (repairedMountEntries !== GIT_EXECUTABLES.size) {
|
||||
throw new Error(
|
||||
`Expected ${GIT_EXECUTABLES.size} Git mount entries, repaired ${repairedMountEntries}`
|
||||
);
|
||||
}
|
||||
|
||||
let headerOffset = tarOffset;
|
||||
let repairedExecutables = 0;
|
||||
|
||||
while (
|
||||
headerOffset + TAR_BLOCK_SIZE <= packageBytes.length &&
|
||||
!isEmptyTarHeader(packageBytes, headerOffset)
|
||||
) {
|
||||
const name = readTarName(packageBytes, headerOffset);
|
||||
const size = parseTarOctal(
|
||||
packageBytes,
|
||||
headerOffset + TAR_SIZE_OFFSET,
|
||||
TAR_SIZE_SIZE
|
||||
);
|
||||
|
||||
if (GIT_EXECUTABLES.has(name)) {
|
||||
packageBytes.write(
|
||||
"0000755\0",
|
||||
headerOffset + TAR_MODE_OFFSET,
|
||||
TAR_MODE_SIZE,
|
||||
"ascii"
|
||||
);
|
||||
writeTarChecksum(packageBytes, headerOffset);
|
||||
repairedExecutables += 1;
|
||||
}
|
||||
|
||||
headerOffset +=
|
||||
TAR_BLOCK_SIZE + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
if (repairedExecutables !== GIT_EXECUTABLES.size) {
|
||||
throw new Error(
|
||||
`Expected ${GIT_EXECUTABLES.size} Git executables, repaired ${repairedExecutables}`
|
||||
);
|
||||
}
|
||||
|
||||
return packageBytes;
|
||||
};
|
||||
|
||||
let cachedGitSoftware: { readonly packagePath: string } | undefined;
|
||||
|
||||
export const getExecutableGitSoftware = (): {
|
||||
readonly packagePath: string;
|
||||
} => {
|
||||
if (cachedGitSoftware) {
|
||||
return cachedGitSoftware;
|
||||
}
|
||||
|
||||
const source = readFileSync(git.packagePath);
|
||||
const repaired = makeGitExecutablesRunnable(source);
|
||||
const digest = createHash("sha256")
|
||||
.update(repaired)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
const cacheDirectory = path.join(tmpdir(), "zopu-agentos-software");
|
||||
const packagePath = path.join(cacheDirectory, `git-${digest}.aospkg`);
|
||||
|
||||
mkdirSync(cacheDirectory, { recursive: true });
|
||||
if (!existsSync(packagePath)) {
|
||||
writeFileSync(packagePath, repaired);
|
||||
}
|
||||
|
||||
cachedGitSoftware = { packagePath };
|
||||
return cachedGitSoftware;
|
||||
};
|
||||
@@ -1,15 +1,16 @@
|
||||
/* eslint-disable max-classes-per-file -- the AgentOS service and its tagged error form one adapter contract. */
|
||||
import codex from "@agentos-software/codex-cli";
|
||||
import git from "@agentos-software/git";
|
||||
import { agentOS as createAgentOsActor } from "@rivet-dev/agentos";
|
||||
import type { AgentOSConfigInput } from "@rivet-dev/agentos";
|
||||
import { Context, Effect, Layer, Schema } from "effect";
|
||||
|
||||
import { getExecutableGitSoftware } from "./agent-os-git-software";
|
||||
|
||||
const withGitSoftware = (
|
||||
config: AgentOSConfigInput<undefined>
|
||||
): AgentOSConfigInput<undefined> => ({
|
||||
...config,
|
||||
software: [git, ...(config.software ?? [])],
|
||||
software: [getExecutableGitSoftware(), ...(config.software ?? [])],
|
||||
});
|
||||
|
||||
export class AgentOsError extends Schema.TaggedErrorClass<AgentOsError>()(
|
||||
@@ -83,7 +84,7 @@ export const createAgentOsActorEffect = Effect.fn("createAgentOsActorEffect")(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Software bundle for a Codex-capable VM: the Codex harness plus git. */
|
||||
export const codexSoftware = [codex, git] as const;
|
||||
export const codexSoftware = [codex, getExecutableGitSoftware()] as const;
|
||||
|
||||
/** Model-provider env that Codex reads inside the VM. Pass this to the session's
|
||||
* `env` when opening a Codex session (`agent: "codex"`). */
|
||||
|
||||
@@ -73,6 +73,16 @@ describe("project workspace primitives", () => {
|
||||
operation._tag === "Exec" && operation.command.includes("git clone")
|
||||
)
|
||||
).toBe(true);
|
||||
const checkoutOperation = plan.operations.find(
|
||||
(operation) => operation._tag === "Exec"
|
||||
);
|
||||
expect(checkoutOperation?._tag).toBe("Exec");
|
||||
if (checkoutOperation?._tag === "Exec") {
|
||||
expect(checkoutOperation.command).not.toContain("--single-branch");
|
||||
expect(checkoutOperation.command).toContain(
|
||||
"checkout -b 'work/issue-7-abc123'"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects unsafe sources and control-file paths", () => {
|
||||
|
||||
@@ -260,17 +260,18 @@ const makeCheckoutCommand = (input: {
|
||||
readonly sourceUrl: string;
|
||||
}): string => {
|
||||
const checkout = shellQuote(input.checkoutPath);
|
||||
const baseBranch = shellQuote(input.baseBranch);
|
||||
const branch = shellQuote(input.branchName);
|
||||
const originBaseBranch = shellQuote(`origin/${input.baseBranch}`);
|
||||
const branchRef = shellQuote(
|
||||
`${input.checkoutPath}/.git/refs/heads/${input.branchName}`
|
||||
);
|
||||
const source = shellQuote(input.sourceUrl);
|
||||
|
||||
return [
|
||||
"set -eu",
|
||||
`mkdir -p ${shellQuote(WORKSPACE_PATH)}`,
|
||||
`if [ ! -d ${checkout}/.git ]; then git clone --branch ${baseBranch} --single-branch ${source} ${checkout}; fi`,
|
||||
`if [ "$(git -C ${checkout} branch --show-current)" != ${branch} ]; then git -C ${checkout} show-ref --verify --quiet refs/heads/${branch} && git -C ${checkout} checkout ${branch} || git -C ${checkout} checkout -b ${branch} ${originBaseBranch}; fi`,
|
||||
`printf '%s\\n' "$(git -C ${checkout} branch --show-current)" "$(git -C ${checkout} rev-parse HEAD)"`,
|
||||
`if [ ! -d ${checkout}/.git ]; then git clone ${source} ${checkout}; fi`,
|
||||
`if [ -f ${branchRef} ]; then git -C ${checkout} checkout ${branch}; else git -C ${checkout} checkout -b ${branch}; fi`,
|
||||
`printf '%s\\n' ${branch} "$(git -C ${checkout} rev-parse HEAD)"`,
|
||||
].join(" && ");
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user