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
13 changes: 13 additions & 0 deletions packages/bash-types/src/bash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { BaseRuntime } from "./runtime";

export interface BashOptions {
/**
* The runtime to use for executing commands
*/
runtime: BaseRuntime;

/**
* The initial working directory
*/
initialCwd?: string;
}
2 changes: 2 additions & 0 deletions packages/bash-types/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { State } from "./state";
export { BaseRuntime, RuntimeResult } from "./runtime";
export { BashOptions } from "./bash";
38 changes: 38 additions & 0 deletions packages/bash-types/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface BaseRuntime {
/**
* Execute a command
*/
executeCommand(code: string): Promise<RuntimeResult>;

/**
* Execute a code
*/
executeCode(language: string, code: string): Promise<RuntimeResult>;

/**
* Execute a file
*/
executeFile(language: string, filePath: string): Promise<RuntimeResult>;

/**
* Resolve a directory path
*/
resolveDirectoryPath(directoryPath: string): Promise<string>;
}

export interface RuntimeResult {
/**
* Standard output
*/
stdout: string;

/**
* Standard error
*/
stderr: string;

/**
* Exit code
*/
exitCode: number;
}
3 changes: 3 additions & 0 deletions packages/bash-wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
"packageManager": "pnpm@10.21.0",
"devDependencies": {
"tsup": "^8.0.0"
},
"dependencies": {
"@capsule-run/bash-types": "workspace:*"
}
}
2 changes: 1 addition & 1 deletion packages/bash-wasm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@

export { WasmRuntime } from "./runtime";
21 changes: 21 additions & 0 deletions packages/bash-wasm/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { BaseRuntime, RuntimeResult } from "@capsule-run/bash-types";

export class WasmRuntime implements BaseRuntime {
constructor() {}

async executeCommand(code: string): Promise<RuntimeResult> {
throw new Error("Method not implemented.");
}

async executeCode(language: string, code: string): Promise<RuntimeResult> {
throw new Error("Method not implemented.");
}

async executeFile(language: string, filePath: string): Promise<RuntimeResult> {
throw new Error("Method not implemented.");
}

async resolveDirectoryPath(directoryPath: string): Promise<string> {
throw new Error("Method not implemented.");
}
}
3 changes: 3 additions & 0 deletions packages/bash/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
"packageManager": "pnpm@10.21.0",
"devDependencies": {
"tsup": "^8.0.0"
},
"dependencies": {
"@capsule-run/bash-types": "workspace:*"
}
}
28 changes: 28 additions & 0 deletions packages/bash/src/core/bash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { BaseRuntime, BashOptions } from "@capsule-run/bash-types";
import { StateManager } from "./stateManager";
import { Filesystem } from "./filesystem";

export class Bash {
private runtime: BaseRuntime;
private filesystem: Filesystem;

public readonly state: StateManager;

constructor({ runtime, initialCwd = "workspace" }: BashOptions) {
this.runtime = runtime;
this.filesystem = new Filesystem(".capsule/session/workspace");
this.state = new StateManager(runtime, initialCwd);

this.filesystem.init();
}

run(command: string) {
this.runtime.executeCommand(command);
}

reset() {
this.filesystem.reset();
this.state.reset();
}

}
35 changes: 35 additions & 0 deletions packages/bash/src/core/filesystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import fs from 'fs';
import path from 'path';

export class Filesystem {
private workspace: string;

constructor(workspace: string) {
this.workspace = workspace;
}

init() {
const directories = ['bin', 'dev', 'etc', 'proc', 'root', 'sys', 'tmp', 'workspace'];

for (const dir of directories) {
fs.mkdirSync(path.join(this.workspace, dir), { recursive: true });
}

const files: Record<string, string> = {
'etc/resolv.conf': 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n',
'etc/os-release': 'NAME="Capsule OS"\nVERSION="1.0"\nID=capsule\n',
'etc/passwd': 'root:x:0:0:root:/root:/bin/bash\n',
'proc/cpuinfo': 'processor\t: 0\nvendor_id\t: CapsuleVirtualCPU\n',
'workspace/README.md': '# Welcome to the Capsule Bash Environment\\n\\nYou are operating inside a secure and minimalist sandbox.\\n\\n## Environment Details:\\n- **OS:** Capsule Bash (Mocked Linux)\\n- **Capabilities:** Standard bash operations are supported, but advanced system calls may be restricted.\\n- **Working Directory:** `/workspace` is your designated safe zone.'
};

for (const [relativePath, content] of Object.entries(files)) {
fs.writeFileSync(path.join(this.workspace, relativePath), content);
}
}

reset() {
fs.rmSync(this.workspace, { recursive: true, force: true });
this.init();
}
}
39 changes: 39 additions & 0 deletions packages/bash/src/core/stateManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import path from 'node:path';
import type { BaseRuntime, State } from '@capsule-run/bash-types';

export class StateManager {
public readonly state: State;
private runtime: BaseRuntime;

constructor(runtime: BaseRuntime, initialCwd: string = 'workspace') {
this.runtime = runtime;
this.state = {
cwd: initialCwd,
env: {},
lastExitCode: 0
};
}

get displayCwd(): string {
return this.state.cwd.startsWith('/') ? this.state.cwd : `/${this.state.cwd}`;
}

public async changeDirectory(targetPath: string): Promise<void> {
const resolvedPath = await this.runtime.resolveDirectoryPath(targetPath);
this.state.cwd = resolvedPath;
}

public setEnv(key: string, value: string): void {
this.state.env[key] = value;
}

public setExitCode(code: number): void {
this.state.lastExitCode = code;
}

public reset() {
this.state.cwd = 'workspace';
this.state.env = {};
this.state.lastExitCode = 0;
}
}
1 change: 1 addition & 0 deletions packages/bash/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Bash } from "./core/bash";
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

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

49 changes: 48 additions & 1 deletion sandboxes/js/__test__/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const SANDBOX = path.resolve(__dirname, '../sandbox.ts');
const WORKSPACE = '__test__/workspace';

const baseState = JSON.stringify({
cwd: '/',
cwd: '.',
env: {},
lastExitCode: 0,
});
Expand Down Expand Up @@ -147,3 +147,50 @@ describe('sandbox.ts – invalid action', () => {
assertFailure(result);
});
});


describe('sandbox.ts – RESOLVE_DIRECTORY_PATH', () => {
it('resolves a directory path', async () => {
const result = await run({
file: SANDBOX,
args: ['RESOLVE_DIRECTORY_PATH', baseState, 'imports'],
mounts: [`${WORKSPACE}::/`],
});

const value = assertSuccess(result);
expect(value).toBe('/imports');
});

it('Should return an error because the directory path does not exist', async () => {
const result = await run({
file: SANDBOX,
args: ['RESOLVE_DIRECTORY_PATH', baseState, '../non-existent-directory'],
mounts: [`${WORKSPACE}::/`],
});

const value = assertFailure(result);
expect(value.message).toContain("Directory path ../non-existent-directory does not exist");
});

it('resolves a directory path', async () => {
const result = await run({
file: SANDBOX,
args: ['RESOLVE_DIRECTORY_PATH', baseState, 'imports/../imports/complex-path-testing'],
mounts: [`${WORKSPACE}::/`],
});

const value = assertSuccess(result);
expect(value).toBe('/imports/complex-path-testing');
});

it('Should works with a different initial cwd', async () => {
const result = await run({
file: SANDBOX,
args: ['RESOLVE_DIRECTORY_PATH', JSON.stringify({ cwd: 'imports', env: {}, lastExitCode: 0 }), 'complex-path-testing'],
mounts: [`${WORKSPACE}::/`],
});

const value = assertSuccess(result);
expect(value).toBe('/imports/complex-path-testing');
});
});
Empty file.
15 changes: 15 additions & 0 deletions sandboxes/js/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ const executeCommand = task(
}
)

export const resolveDirectoryPath = task(
{ name: "resolveDirectoryPath", compute: "LOW", ram: "32MB" },
async (state: State, targetPath: string) => {
process.chdir(state.cwd);

if (!fs.existsSync(targetPath)) {
throw new Error(`Directory path ${targetPath} does not exist`);
}

return '/' + path.resolve(targetPath);
}
)

export const main = task(
{ name: "main", compute: "HIGH" },
async (action: string, state: string, ...args: string[]): Promise<unknown> => {
Expand All @@ -131,6 +144,8 @@ export const main = task(
response = await executeCode(parsedState, parsedArgs[0]);
} else if (action === "EXECUTE_FILE") {
response = await executeFile(parsedState, parsedArgs[0], parsedArgs.slice(1));
} else if (action === "RESOLVE_DIRECTORY_PATH") {
response = await resolveDirectoryPath(parsedState, parsedArgs[0]);
} else {
throw new Error(`Invalid action: ${action}`);
}
Expand Down
Loading