blob: 976e0ae004f6c667ce2c1563836bda0033068711 [file]
type RuntimeHostCliError = { kind: 'error'; message: string; exitCode: number };
export type RuntimeHostCliCommand =
| { kind: 'runtime-host-manage' }
| {
kind: 'runtime-host-serve';
rootPath?: string;
managedConfigId?: string;
expectedConfigRevision?: string;
startAttemptId?: string;
json: boolean;
websocket?: {
host: string;
port: number;
path?: string;
tlsCertificatePath?: string;
tlsPrivateKeyPath?: string;
allowedOrigins?: string[];
allowInsecureRemote?: boolean;
};
}
| {
kind: 'runtime-host-access-issue';
rootPath?: string;
principalKind: 'remote_owner' | 'capability_provider';
principalId: string;
operationGrants: string[];
canPublishClientCapabilities: boolean;
canUseHostPaths: boolean;
preset?: 'desktop-client' | 'terminal-client';
}
| { kind: 'runtime-host-access-revoke'; rootPath?: string; credentialId: string }
| { kind: 'runtime-host-project-list'; rootPath?: string }
| { kind: 'runtime-host-project-add'; rootPath?: string; path: string }
| {
kind: 'runtime-host-capability-provider-serve';
url: string;
mcpConfigPath: string;
expectedRootId: string;
credentialEnv?: string;
clientIdentityPath?: string;
}
| { kind: 'runtime-host-profile-list' }
| {
kind: 'runtime-host-profile-set';
id: string;
name: string;
transport:
| { kind: 'tls'; url: string }
| {
kind: 'plaintext';
url: string;
acknowledgement: 'plaintext-bearer-v1';
}
| {
kind: 'ssh';
destination: string;
sshPort?: number;
remotePort: number;
websocketPath: string;
};
expectedRootId: string;
credentialEnv?: string;
}
| { kind: 'runtime-host-profile-remove'; id: string }
| RuntimeHostCliError;
export function parseRuntimeHostCommand(argv: string[]): RuntimeHostCliCommand {
if (argv[0] === 'manage' || argv[0] === 'bootstrap') {
return argv.length === 1
? { kind: 'runtime-host-manage' }
: error(`Unexpected argument: ${argv[1] ?? ''}`);
}
if (argv[0] === 'serve') return parseServeCommand(argv.slice(1));
if (argv[0] === 'access') return parseAccessCommand(argv.slice(1));
if (argv[0] === 'project') return parseProjectCommand(argv.slice(1));
if (argv[0] === 'capability-provider') {
return parseCapabilityProviderCommand(argv.slice(1));
}
if (argv[0] === 'profile') return parseProfileCommand(argv.slice(1));
return error(
argv[0]
? `Unexpected runtime-host command: ${argv[0]}`
: 'runtime-host requires the manage, serve, access, project, profile, or capability-provider command',
);
}
function parseProjectCommand(argv: string[]): RuntimeHostCliCommand {
const action = argv[0];
if (action !== 'list' && action !== 'add') {
return error(
action
? `Unexpected runtime-host project command: ${action}`
: 'runtime-host project requires the list or add command',
);
}
let rootPath: string | undefined;
let path: string | undefined;
for (let index = 1; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === '--root') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
rootPath = parsed;
index += 1;
continue;
}
if (action === 'add' && path === undefined) {
path = argument;
continue;
}
return error(`Unexpected argument: ${argument ?? ''}`);
}
if (action === 'list') {
return { kind: 'runtime-host-project-list', ...(rootPath ? { rootPath } : {}) };
}
if (!path) return error('runtime-host project add requires a path');
return { kind: 'runtime-host-project-add', path, ...(rootPath ? { rootPath } : {}) };
}
function parseProfileCommand(argv: string[]): RuntimeHostCliCommand {
const action = argv[0];
if (action === 'list') {
return argv.length === 1
? { kind: 'runtime-host-profile-list' }
: error(`Unexpected argument: ${argv[1] ?? ''}`);
}
if (action === 'remove') {
if (argv[1] !== '--id') return error('runtime-host profile remove requires --id');
const id = optionValue(argv, 1, '--id');
if (typeof id !== 'string') return id;
return argv.length === 3
? { kind: 'runtime-host-profile-remove', id }
: error(`Unexpected argument: ${argv[3] ?? ''}`);
}
if (action !== 'set') {
return error(
action
? `Unexpected runtime-host profile command: ${action}`
: 'runtime-host profile requires the list, set, or remove command',
);
}
let id: string | undefined;
let name: string | undefined;
let tlsUrl: string | undefined;
let plaintextUrl: string | undefined;
let acknowledgePlaintext = false;
let sshDestination: string | undefined;
let sshPort: number | undefined;
let sshRemotePort: number | undefined;
let sshWebSocketPath = '/runtime-host';
let sshWebSocketPathConfigured = false;
let expectedRootId: string | undefined;
let credentialEnv: string | undefined;
for (let index = 1; index < argv.length; index += 1) {
const argument = argv[index];
if (
argument !== '--id' &&
argument !== '--name' &&
argument !== '--tls-url' &&
argument !== '--plaintext-url' &&
argument !== '--ssh-destination' &&
argument !== '--ssh-port' &&
argument !== '--ssh-remote-port' &&
argument !== '--ssh-websocket-path' &&
argument !== '--expected-root' &&
argument !== '--credential-env' &&
argument !== '--acknowledge-plaintext'
) {
return error(`Unexpected argument: ${argument ?? ''}`);
}
if (argument === '--acknowledge-plaintext') {
acknowledgePlaintext = true;
continue;
}
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
if (argument === '--id') id = parsed;
if (argument === '--name') name = parsed;
if (argument === '--tls-url') tlsUrl = parsed;
if (argument === '--plaintext-url') plaintextUrl = parsed;
if (argument === '--ssh-destination') sshDestination = parsed;
if (argument === '--ssh-port') sshPort = Number(parsed);
if (argument === '--ssh-remote-port') sshRemotePort = Number(parsed);
if (argument === '--ssh-websocket-path') {
sshWebSocketPath = parsed;
sshWebSocketPathConfigured = true;
}
if (argument === '--expected-root') expectedRootId = parsed;
if (argument === '--credential-env') credentialEnv = parsed;
index += 1;
}
if (!id) return error('--id is required');
if (!name) return error('--name is required');
if ((tlsUrl ? 1 : 0) + (plaintextUrl ? 1 : 0) + (sshDestination ? 1 : 0) !== 1) {
return error('exactly one of --tls-url, --plaintext-url, or --ssh-destination is required');
}
if (plaintextUrl && !acknowledgePlaintext) {
return error('--plaintext-url requires --acknowledge-plaintext');
}
if (!plaintextUrl && acknowledgePlaintext) {
return error('--acknowledge-plaintext requires --plaintext-url');
}
if (
!sshDestination &&
(sshPort !== undefined || sshRemotePort !== undefined || sshWebSocketPathConfigured)
) {
return error('SSH options require --ssh-destination');
}
if (sshDestination && !sshRemotePort) {
return error('--ssh-destination requires --ssh-remote-port');
}
if (sshPort !== undefined && (!Number.isInteger(sshPort) || sshPort < 1 || sshPort > 65_535)) {
return error('--ssh-port must be an integer between 1 and 65535');
}
if (
sshRemotePort !== undefined &&
(!Number.isInteger(sshRemotePort) || sshRemotePort < 1 || sshRemotePort > 65_535)
) {
return error('--ssh-remote-port must be an integer between 1 and 65535');
}
if (!expectedRootId) return error('--expected-root is required');
return {
kind: 'runtime-host-profile-set',
id,
name,
transport: tlsUrl
? { kind: 'tls', url: tlsUrl }
: plaintextUrl
? {
kind: 'plaintext',
url: plaintextUrl,
acknowledgement: 'plaintext-bearer-v1',
}
: {
kind: 'ssh',
destination: sshDestination!,
...(sshPort === undefined ? {} : { sshPort }),
remotePort: sshRemotePort!,
websocketPath: sshWebSocketPath,
},
expectedRootId,
...(credentialEnv ? { credentialEnv } : {}),
};
}
function parseCapabilityProviderCommand(argv: string[]): RuntimeHostCliCommand {
if (argv[0] !== 'serve') {
return error(
argv[0]
? `Unexpected runtime-host capability-provider command: ${argv[0]}`
: 'runtime-host capability-provider requires the serve command',
);
}
let url: string | undefined;
let mcpConfigPath: string | undefined;
let expectedRootId: string | undefined;
let credentialEnv: string | undefined;
let clientIdentityPath: string | undefined;
for (let index = 1; index < argv.length; index += 1) {
const argument = argv[index];
if (
argument === '--url' ||
argument === '--mcp-config' ||
argument === '--expected-root' ||
argument === '--credential-env' ||
argument === '--client-identity'
) {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
if (argument === '--url') url = parsed;
if (argument === '--mcp-config') mcpConfigPath = parsed;
if (argument === '--expected-root') expectedRootId = parsed;
if (argument === '--credential-env') credentialEnv = parsed;
if (argument === '--client-identity') clientIdentityPath = parsed;
index += 1;
continue;
}
return error(`Unexpected argument: ${argument ?? ''}`);
}
if (!url) return error('--url is required');
if (!mcpConfigPath) return error('--mcp-config is required');
if (!expectedRootId) return error('--expected-root is required');
return {
kind: 'runtime-host-capability-provider-serve',
url,
mcpConfigPath,
expectedRootId,
...(credentialEnv ? { credentialEnv } : {}),
...(clientIdentityPath ? { clientIdentityPath } : {}),
};
}
function parseServeCommand(argv: string[]): RuntimeHostCliCommand {
let rootPath: string | undefined;
let managedConfigId: string | undefined;
let expectedConfigRevision: string | undefined;
let startAttemptId: string | undefined;
let json = false;
let websocketHost = '127.0.0.1';
let websocketConfigured = false;
let websocketPort: number | undefined;
let websocketPath: string | undefined;
let tlsCertificatePath: string | undefined;
let tlsPrivateKeyPath: string | undefined;
let allowInsecureRemote = false;
const allowedOrigins: string[] = [];
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === '--json') {
json = true;
continue;
}
if (argument === '--allow-insecure-remote') {
allowInsecureRemote = true;
websocketConfigured = true;
continue;
}
if (argument === '--root') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
rootPath = parsed;
index += 1;
continue;
}
if (argument === '--managed-config') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
managedConfigId = parsed;
index += 1;
continue;
}
if (argument === '--expected-config-revision') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
expectedConfigRevision = parsed;
index += 1;
continue;
}
if (argument === '--start-attempt') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
startAttemptId = parsed;
index += 1;
continue;
}
if (argument === '--websocket-host') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
websocketHost = parsed;
websocketConfigured = true;
index += 1;
continue;
}
if (argument === '--websocket-port') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
websocketPort = Number(parsed);
websocketConfigured = true;
if (!Number.isInteger(websocketPort) || websocketPort < 1 || websocketPort > 65_535) {
return error('--websocket-port must be an integer between 1 and 65535');
}
index += 1;
continue;
}
if (argument === '--websocket-path') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
websocketPath = parsed;
websocketConfigured = true;
index += 1;
continue;
}
if (argument === '--tls-certificate' || argument === '--tls-private-key') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
if (argument === '--tls-certificate') tlsCertificatePath = parsed;
else tlsPrivateKeyPath = parsed;
websocketConfigured = true;
index += 1;
continue;
}
if (argument === '--allow-origin') {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
allowedOrigins.push(parsed);
websocketConfigured = true;
index += 1;
continue;
}
return error(`Unexpected argument: ${argument ?? ''}`);
}
if ((tlsCertificatePath === undefined) !== (tlsPrivateKeyPath === undefined)) {
return error('--tls-certificate and --tls-private-key must be provided together');
}
if (allowInsecureRemote && tlsCertificatePath !== undefined) {
return error('--allow-insecure-remote cannot be combined with TLS');
}
if (websocketConfigured && websocketPort === undefined) {
return error('--websocket-port is required for WebSocket options');
}
if (managedConfigId !== undefined && (rootPath !== undefined || websocketConfigured)) {
return error('--managed-config cannot be combined with --root or WebSocket options');
}
if (expectedConfigRevision !== undefined && managedConfigId === undefined) {
return error('--expected-config-revision requires --managed-config');
}
if (startAttemptId !== undefined && managedConfigId === undefined) {
return error('--start-attempt requires --managed-config');
}
if (
managedConfigId !== undefined &&
(expectedConfigRevision === undefined || startAttemptId === undefined)
) {
return error('--managed-config requires --expected-config-revision and --start-attempt');
}
return {
kind: 'runtime-host-serve',
json,
...(managedConfigId ? { managedConfigId } : {}),
...(expectedConfigRevision ? { expectedConfigRevision } : {}),
...(startAttemptId ? { startAttemptId } : {}),
...(rootPath ? { rootPath } : {}),
...(websocketPort === undefined
? {}
: {
websocket: {
host: websocketHost,
port: websocketPort,
...(websocketPath ? { path: websocketPath } : {}),
...(tlsCertificatePath ? { tlsCertificatePath } : {}),
...(tlsPrivateKeyPath ? { tlsPrivateKeyPath } : {}),
...(allowedOrigins.length > 0 ? { allowedOrigins } : {}),
...(allowInsecureRemote ? { allowInsecureRemote: true } : {}),
},
}),
};
}
function parseAccessCommand(argv: string[]): RuntimeHostCliCommand {
const action = argv[0];
if (action !== 'issue' && action !== 'revoke') {
return error(
action
? `Unexpected runtime-host access command: ${action}`
: 'runtime-host access requires the issue or revoke command',
);
}
let rootPath: string | undefined;
let principalId: string | undefined;
let principalKind: 'remote_owner' | 'capability_provider' = 'remote_owner';
let principalKindSpecified = false;
let credentialId: string | undefined;
const operationGrants: string[] = [];
let canPublishClientCapabilities = false;
let canUseHostPaths = false;
let preset: 'desktop-client' | 'terminal-client' | undefined;
for (let index = 1; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === '--publish-client-capabilities') {
canPublishClientCapabilities = true;
continue;
}
if (argument === '--allow-host-paths') {
canUseHostPaths = true;
continue;
}
if (
argument === '--root' ||
argument === '--kind' ||
argument === '--preset' ||
argument === '--principal' ||
argument === '--grant' ||
argument === '--credential'
) {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
if (argument === '--root') rootPath = parsed;
if (argument === '--kind') {
if (parsed !== 'remote-owner' && parsed !== 'capability-provider') {
return error('--kind must be remote-owner or capability-provider');
}
principalKind = parsed === 'remote-owner' ? 'remote_owner' : 'capability_provider';
principalKindSpecified = true;
}
if (argument === '--preset') {
if (parsed !== 'desktop-client' && parsed !== 'terminal-client') {
return error('--preset must be desktop-client or terminal-client');
}
preset = parsed;
}
if (argument === '--principal') principalId = parsed;
if (argument === '--grant') operationGrants.push(parsed);
if (argument === '--credential') credentialId = parsed;
index += 1;
continue;
}
return error(`Unexpected argument: ${argument ?? ''}`);
}
if (action === 'issue') {
if (!principalId) return error('--principal is required');
if (credentialId) return error('--credential is only valid for access revoke');
if (
preset &&
(principalKindSpecified ||
operationGrants.length > 0 ||
canPublishClientCapabilities ||
canUseHostPaths)
) {
return error('--preset cannot be combined with --kind, --grant, or authority flags');
}
if (preset) {
return {
kind: 'runtime-host-access-issue',
...(rootPath ? { rootPath } : {}),
principalKind: 'remote_owner',
principalId,
operationGrants,
canPublishClientCapabilities: false,
canUseHostPaths: false,
preset,
};
}
if (principalKind === 'capability_provider') {
const requiredGrants = ['client.capability.replace', 'client.capability.unregister'];
if (canUseHostPaths) return error('A capability provider cannot use Host paths');
if (operationGrants.length === 0) operationGrants.push(...requiredGrants);
if (
operationGrants.length !== requiredGrants.length ||
requiredGrants.some((grant) => !operationGrants.includes(grant))
) {
return error('A capability provider may grant only Client Capability publication');
}
canPublishClientCapabilities = true;
} else if (operationGrants.length === 0) {
return error('At least one --grant is required');
}
return {
kind: 'runtime-host-access-issue',
...(rootPath ? { rootPath } : {}),
principalKind,
principalId,
operationGrants,
canPublishClientCapabilities,
canUseHostPaths,
};
}
if (!credentialId) return error('--credential is required');
if (
principalId ||
principalKindSpecified ||
operationGrants.length > 0 ||
canPublishClientCapabilities ||
canUseHostPaths ||
preset
) {
return error('Issue-only access options are not valid for revoke');
}
return {
kind: 'runtime-host-access-revoke',
...(rootPath ? { rootPath } : {}),
credentialId,
};
}
function optionValue(argv: string[], index: number, option: string): string | RuntimeHostCliError {
const value = argv[index + 1];
return !value || value.startsWith('-') ? error(`${option} requires a value`) : value;
}
function error(message: string): RuntimeHostCliError {
return { kind: 'error', message, exitCode: 2 };
}