Skip to content
Closed
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
62 changes: 32 additions & 30 deletions bun.lock

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

4 changes: 2 additions & 2 deletions docs/architecture-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,15 @@ The controller CLI displays this code; the target CLI prompts the operator to en

## ADR-011: Remote shell as separate package with explicit shell permission

**Decision:** The remote shell feature ships as `@authmesh/shell`, a separate npm package with separate binaries (`amesh-agent`, `amesh-shell`). Shell access requires explicit `amesh grant --shell` after pairing.
**Decision:** The shell client (`amesh shell`) is a subcommand in `@authmesh/cli`. The agent daemon (`amesh-agent`) ships as `@authmesh/agent`, a separate package for the target server. Shell access requires explicit `amesh grant --shell` after pairing.

**Why:**

1. **Security boundary:** Installing `@authmesh/sdk` for HTTP API auth must never pull in PTY code or an agent daemon. The attack surface for API signing and shell access are fundamentally different.

2. **Explicit consent:** Pairing for API authentication (`amesh invite`) does not grant shell access. A `permissions.shell` flag in the allow list defaults to `false`. The target admin must explicitly run `amesh grant <device-id> --shell`. This prevents implicit privilege escalation.

3. **Separate binaries:** `amesh-agent` and `amesh-shell` are distinct from `amesh` (the CLI). Users opt into shell capability by installing a separate package.
3. **Controller in CLI, agent separate:** `amesh shell` is a subcommand of the existing CLI. Only the server needs `@authmesh/agent` — the developer's laptop never has daemon code.

**Security design choices:**

Expand Down
18 changes: 9 additions & 9 deletions landpage/src/routes/docs/remote-shell/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@
<!-- Install -->
<section class="py-8">
<h2 id="install" class="scroll-mt-20 text-xl font-semibold text-zinc-50">Install</h2>
<p class="mt-2 text-zinc-400">The shell feature is a separate package from the CLI.</p>
<p class="mt-2 text-zinc-400">The CLI includes the shell client. The server needs the agent package.</p>
<div class="mt-4">
<CodeBlock code={`<span class="text-zinc-500"># Install the shell package (agent + shell client)</span>
brew install ameshdev/tap/amesh-shell
<span class="text-zinc-500"># or</span>
npm install -g @authmesh/shell
<CodeBlock code={`<span class="text-zinc-500"># On your laptop (controller) — CLI includes amesh shell</span>
brew install ameshdev/tap/amesh

<span class="text-zinc-500"># You also need the CLI for pairing and permissions</span>
brew install ameshdev/tap/amesh`} />
<span class="text-zinc-500"># On the server (target) — agent daemon</span>
brew install ameshdev/tap/amesh-agent
<span class="text-zinc-500"># or</span>
npm install -g @authmesh/agent`} />
</div>
</section>

Expand Down Expand Up @@ -98,7 +98,7 @@ amesh-agent start --relay wss://relay.authmesh.dev/ws --idle-timeout 60`} />

<h3 class="mt-4 text-sm font-semibold uppercase tracking-wide text-zinc-500">Interactive shell</h3>
<div class="mt-3">
<CodeBlock code={`<span class="text-zinc-500">$</span> amesh-shell prod-api
<CodeBlock code={`<span class="text-zinc-500">$</span> amesh shell prod-api
Connecting to prod-api (am_7f2e8a1b)...
Connected. Shell session started.

Expand All @@ -110,7 +110,7 @@ user

<h3 class="mt-6 text-sm font-semibold uppercase tracking-wide text-zinc-500">Single command</h3>
<div class="mt-3">
<CodeBlock code={`<span class="text-zinc-500">$</span> amesh-shell prod-api -c "df -h"
<CodeBlock code={`<span class="text-zinc-500">$</span> amesh shell prod-api -c "df -h"
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 12G 35G 26% /`} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion landpage/src/routes/use-cases/remote-shell/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

Waiting for shell requests...` },
{ filename: 'Terminal (controller)', code: `<span class="text-zinc-500"># On your laptop — open a shell</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh-shell prod-api</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh shell prod-api</span>

Connecting to prod-api (<span class="text-emerald-400">am_7f2e8a1b</span>)...
Connected. Shell session started.
Expand Down
57 changes: 57 additions & 0 deletions packages/agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# @authmesh/agent

Agent daemon for [amesh](https://github.com/ameshdev/amesh) remote shell --- secure remote access using device-bound identity. Install on the server (target) to accept shell connections from paired controllers.

**Includes the full amesh CLI** — one install on the server gives you everything (init, pair, grant, agent).

## Install

```bash
brew install ameshdev/tap/amesh-agent
# or
npm install -g @authmesh/agent
```

## Setup

```bash
# Create identity + pair with controller (one-time)
amesh init --name "prod-api"
amesh listen
# Controller runs: amesh invite <code>

# Grant shell access to the controller
amesh grant am_3d9f1a2e --shell

# Start the agent daemon
amesh-agent start
```

## Usage

```bash
amesh-agent start # foreground
amesh-agent start --idle-timeout 60 # custom timeout (minutes)
amesh-agent start --relay wss://... # custom relay
amesh-agent start --allow-root # run as root (danger)
```

## Security

- Shell access requires explicit `amesh grant --shell` (not automatic from pairing)
- End-to-end encrypted (ChaCha20-Poly1305, ephemeral ECDH per session)
- Refuses to run as root without `--allow-root`
- Per-controller session limits
- HMAC-sealed allow list with tamper detection

## Environment variables

| Variable | Description |
|----------|-------------|
| `AUTH_MESH_DIR` | Override `~/.amesh/` directory |
| `AUTH_MESH_PASSPHRASE` | Passphrase for encrypted-file backend |
| `AMESH_RELAY_URL` | Override default relay URL |

## License

[MIT](https://github.com/ameshdev/amesh/blob/main/LICENSE)
10 changes: 6 additions & 4 deletions packages/shell/package.json → packages/agent/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "@authmesh/shell",
"name": "@authmesh/agent",
"version": "0.2.0",
"description": "Secure remote shell for amesh — SSH-like access with device-bound identity",
"description": "amesh agent daemon — remote shell target with device-bound identity",
"type": "module",
"license": "MIT",
"author": "Yair Etzion",
"repository": {
"type": "git",
"url": "https://github.com/ameshdev/amesh.git",
"directory": "packages/shell"
"directory": "packages/agent"
},
"homepage": "https://github.com/ameshdev/amesh",
"keywords": [
Expand All @@ -24,7 +24,7 @@
},
"bin": {
"amesh-agent": "./dist/commands/agent-start.js",
"amesh-shell": "./dist/commands/shell.js"
"amesh": "./dist/cli-proxy.js"
},
"exports": {
".": {
Expand All @@ -41,8 +41,10 @@
"clean": "rm -rf dist *.tsbuildinfo"
},
"dependencies": {
"@authmesh/cli": "workspace:*",
"@authmesh/core": "workspace:*",
"@authmesh/keystore": "workspace:*",
"@oclif/core": "^4.0.0",
"@noble/curves": "2.0.1",
"@noble/ciphers": "2.1.1",
"@noble/hashes": "2.0.1"
Expand Down
9 changes: 9 additions & 0 deletions packages/shell/src/agent.ts → packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,16 @@ export async function startAgent(opts: AgentOptions): Promise<void> {
const maxSessionsPerController = 1;
const controllerSessions = new Map<string, number>();

const shellControllers = (await allowList.read()).devices.filter(
(d) => d.role === 'controller' && d.permissions?.shell,
).length;

console.log('');
console.warn('[amesh-agent] WARNING: This device is now accepting remote shell connections from authorized controllers.');
console.log(`[amesh-agent] Device: ${identity.deviceId} (${identity.friendlyName})`);
console.log(`[amesh-agent] Authorized controllers with shell access: ${shellControllers}`);
console.log(`[amesh-agent] Run \`amesh list\` to see who has access.`);
console.log('');
console.log(`[amesh-agent] Connecting to relay: ${opts.relayUrl}`);

// Connect to relay with reconnect
Expand Down
14 changes: 14 additions & 0 deletions packages/agent/src/cli-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bun
/**
* Re-exports the amesh CLI so that `npm install -g @authmesh/agent` provides the `amesh` command.
* The agent package is a superset of the CLI.
*/
import { execute } from '@oclif/core';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

// Resolve to @authmesh/cli's dist directory for oclif command discovery
const cliPkg = import.meta.resolve('@authmesh/cli/package.json');
const cliDir = dirname(fileURLToPath(cliPkg));

await execute({ dir: join(cliDir, 'dist') });
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"bin": "amesh"
},
"files": [
"dist"
"dist",
"package.json"
],
"scripts": {
"build": "tsc -b",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default class Grant extends Command {
if (flags.shell) {
this.log(' Shell access: granted');
this.log('');
this.log(' This device can now open remote shells via amesh-shell.');
this.log(' This device can now open remote shells via `amesh shell`.');
} else {
this.log(' Shell access: revoked');
}
Expand Down
38 changes: 38 additions & 0 deletions packages/cli/src/commands/shell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Command, Args, Flags } from '@oclif/core';

export default class Shell extends Command {
static override description = 'Open a remote shell to a paired device';

static override args = {
device: Args.string({
description: 'Device ID (am_...) or friendly name of the target',
required: true,
}),
};

static override flags = {
command: Flags.string({
char: 'c',
description: 'Run a single command and exit',
}),
relay: Flags.string({
char: 'r',
description: 'Relay server URL',
default: 'wss://relay.authmesh.dev/ws',
env: 'AMESH_RELAY_URL',
}),
};

async run(): Promise<void> {
const { args, flags } = await this.parse(Shell);

const { connectShell } = await import('../shell-client.js');
const exitCode = await connectShell({
target: args.device,
relayUrl: flags.relay,
command: flags.command,
});

this.exit(exitCode);
}
}
Loading
Loading