Skip to content
Open
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
47 changes: 47 additions & 0 deletions crates/execution/assets/runners/wasm-runner.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3065,6 +3065,21 @@ const HOST_NET_SO_ERROR = 4;
const HOST_NET_SO_RCVTIMEO_64 = 20;
const HOST_NET_SO_RCVTIMEO_32 = 66;
const HOST_NET_TIMEVAL_BYTES = 16;
// Performance/QoS socket options that guests may set but the host transport
// neither needs nor can honor per-socket: Node's net sockets already run
// with sensible defaults, and DSCP/traffic-class marking is not observable
// through the adapter. Accepted and ignored (values from the patched
// wasi-libc headers, matching Linux): setsockopt(2) succeeds, matching a
// Linux host where these are best-effort hints. OpenSSH sets all four on
// every connection (ssh_packet_set_tos / set_nodelay in opacket/misc) and
// treats failure as per-connection stderr noise.
const HOST_NET_SO_KEEPALIVE = 9; // SOL_SOCKET, socket(7)
const HOST_NET_IPPROTO_IP = 0;
const HOST_NET_IP_TOS = 1; // ip(7)
const HOST_NET_IPPROTO_TCP = 6;
const HOST_NET_TCP_NODELAY = 1; // tcp(7)
const HOST_NET_IPPROTO_IPV6 = 41;
const HOST_NET_IPV6_TCLASS = 67; // ipv6(7)

function hostNetSocketBaseType(socket) {
return Number(socket?.sockType ?? 0) & HOST_NET_SOCKET_TYPE_MASK;
Expand All @@ -3074,6 +3089,35 @@ function hostNetSockoptKind(level, optname, optvalLen) {
const normalizedLevel = Number(level) >>> 0;
const normalizedOptname = Number(optname) >>> 0;
const normalizedOptvalLen = Number(optvalLen) >>> 0;
// Accept-and-ignore QoS/keepalive/nagle hints (see constant block above).
// Option values are plain ints; accept any sane small buffer.
if (normalizedOptvalLen >= 1 && normalizedOptvalLen <= 16) {
if (
(normalizedLevel === HOST_NET_SOL_SOCKET ||
normalizedLevel === HOST_NET_WASI_SOL_SOCKET) &&
normalizedOptname === HOST_NET_SO_KEEPALIVE
) {
return 'ignore';
}
if (
normalizedLevel === HOST_NET_IPPROTO_TCP &&
normalizedOptname === HOST_NET_TCP_NODELAY
) {
return 'ignore';
}
if (
normalizedLevel === HOST_NET_IPPROTO_IP &&
normalizedOptname === HOST_NET_IP_TOS
) {
return 'ignore';
}
if (
normalizedLevel === HOST_NET_IPPROTO_IPV6 &&
normalizedOptname === HOST_NET_IPV6_TCLASS
) {
return 'ignore';
}
}
if (
normalizedLevel !== HOST_NET_SOL_SOCKET &&
normalizedLevel !== HOST_NET_WASI_SOL_SOCKET
Expand Down Expand Up @@ -3898,6 +3942,9 @@ const hostNetImport = {
if (sockoptKind == null) {
return WASI_ERRNO_INVAL;
}
if (sockoptKind === 'ignore') {
return WASI_ERRNO_SUCCESS;
}
try {
const timeoutMs = parseHostNetTimevalMs(readGuestBytes(optvalPtr, optvalLen));
if (timeoutMs == null && readGuestBytes(optvalPtr, optvalLen).some((byte) => byte !== 0)) {
Expand Down
2 changes: 1 addition & 1 deletion crates/execution/src/node_import_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const NODE_IMPORT_CACHE_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_PATH";
const NODE_IMPORT_CACHE_LOADER_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_LOADER_PATH";
const NODE_IMPORT_CACHE_SCHEMA_VERSION: &str = "1";
const NODE_IMPORT_CACHE_LOADER_VERSION: &str = "8";
const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "97";
const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "98";
const NODE_IMPORT_CACHE_DIR_PREFIX: &str = "agentos-node-import-cache";
const DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT: Duration = Duration::from_secs(30);
const PYODIDE_DIST_DIR: &str = "pyodide-dist";
Expand Down
5 changes: 3 additions & 2 deletions packages/agentos/src/generated/actor-actions.generated.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @generated by agentos-actor-plugin. Do not edit.
// This file is committed so package builds do not need to compile the native
// plugin just to regenerate TypeScript action types.
// This file is generated locally and is intentionally not committed to Git.
// Regenerated by the agentos-actor-plugin build script; build the crate
// (e.g. `cargo build -p agentos-actor-plugin`) to refresh it.
import type {
ExecResult,
PermissionReply,
Expand Down
Binary file added packages/runtime-core/commands/ssh
Binary file not shown.
34 changes: 34 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 16 additions & 3 deletions software/git/test/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const hasHostGit = spawnSync('git', ['--version'], { stdio: 'ignore' }).status =
// git-remote-https aliases to it.
const hasGitHttpHelper =
hasGit && existsSync(resolve(COMMANDS_DIR, 'git-remote-http'));
// The real OpenSSH client (software/ssh) lights up git-over-ssh; its presence
// changes how ssh:// remotes fail when unreachable.
const hasSshClient = existsSync(resolve(COMMANDS_DIR, 'ssh'));

const gitConfig = [
'-c safe.directory=*',
Expand Down Expand Up @@ -428,12 +431,22 @@ describeIf(hasGit, 'git command', () => {
expect(result.stderr).not.toContain('GitSubcommandUnsupported');
});

it('clone rejects SSH-style remotes with a real Git spawn failure', async () => {
it('clone over ssh:// reaches the real ssh client and surfaces its transport error', async () => {
({ kernel, dispose } = await createGitKernel());

const result = await kernel.exec(git('clone git@github.com:rivet-dev/agentos.git /tmp/clone'));
// Port 1 on loopback is never exempted for this kernel, so the real
// OpenSSH client (now on PATH — git connect.c execs `ssh`) fails with a
// genuine connection error instead of a spawn failure. Full git-over-ssh
// success coverage lives in software/ssh/test/ssh.test.ts.
const result = await kernel.exec(git('clone ssh://git@127.0.0.1:1/repo.git /tmp/clone'));
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toMatch(/cannot run ssh|unable to fork|ssh|fatal/i);
if (hasSshClient) {
// The error must come from ssh's transport, proving git spawned it.
expect(result.stderr).toMatch(/ssh:|Connection closed|Could not read from remote repository/i);
expect(result.stderr).not.toMatch(/cannot run ssh|unable to fork/i);
} else {
expect(result.stderr).toMatch(/cannot run ssh|unable to fork|ssh|fatal/i);
}
expect(result.stderr).not.toContain('GitSubcommandUnsupported');
});

Expand Down
12 changes: 12 additions & 0 deletions software/ssh/agentos-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"commands": [
"ssh"
],
"registry": {
"title": "ssh",
"description": "OpenSSH client for remote command execution and git-over-ssh.",
"priority": 55,
"image": "/images/registry/ssh.svg",
"category": "networking"
}
}
35 changes: 35 additions & 0 deletions software/ssh/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@agentos-software/ssh",
"version": "0.0.1",
"type": "module",
"license": "Apache-2.0",
"description": "OpenSSH client for AgentOS VMs (batch/key-based; also powers git-over-ssh)",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"!dist/package",
"!dist/package.tar"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build",
"check-types": "tsc --noEmit",
"test": "vitest run test/ --passWithNoTests"
},
"devDependencies": {
"@agentos-software/manifest": "workspace:*",
"@rivet-dev/agentos-toolchain": "workspace:*",
"@types/node": "^22.10.2",
"@types/ssh2": "^1.15.1",
"typescript": "^5.9.2",
"@agentos/test-harness": "workspace:*",
"ssh2": "^1.16.0",
"vitest": "^2.1.9"
}
}
5 changes: 5 additions & 0 deletions software/ssh/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { SoftwarePackageRef } from "@agentos-software/manifest";

const packagePath = new URL("./package.aospkg", import.meta.url).pathname;

export default { packagePath } satisfies SoftwarePackageRef;
Loading