源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
encodedTerminalInputActionsByteLength,
|
||||
isWellFormedTerminalInput,
|
||||
parseTerminalInputAction,
|
||||
type TerminalInputAction,
|
||||
} from '@maka/core/terminal-input';
|
||||
|
||||
import { isShellRunId, SHELL_RUN_ID_MAX_CHARS, type ShellRunStore } from '@maka/core/shell-run';
|
||||
|
||||
import { type ShellRunUpdate, type ToolResultContent } from '@maka/core/events';
|
||||
|
||||
import type { ShellPlan } from './shell-detect.js';
|
||||
import type { ChildFdInput } from './child-fd-input.js';
|
||||
import type { SandboxType } from './sandbox/types.js';
|
||||
|
||||
export const DEFAULT_BASH_TIMEOUT_MS = 120_000;
|
||||
export const MAX_FOREGROUND_BASH_TIMEOUT_MS = 10 * 60 * 1_000;
|
||||
export const MAX_WRITE_STDIN_INPUT_BYTES = 64 * 1024;
|
||||
export const MAX_WRITE_STDIN_ACTIONS = 64;
|
||||
export const MIN_PTY_COLS = 2;
|
||||
export const MAX_PTY_COLS = 240;
|
||||
export const MIN_PTY_ROWS = 1;
|
||||
export const MAX_PTY_ROWS = 100;
|
||||
export const MAX_SHELL_RUN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
||||
export const DEFAULT_MAX_LIVE_SHELL_RUNS = 64;
|
||||
export const DEFAULT_MAX_LIVE_PTY_RUNS = 8;
|
||||
export const DEFAULT_SHELL_RUN_FLUSH_INTERVAL_MS = 1_000;
|
||||
export const DEFAULT_SHELL_RUN_FLUSH_BYTES = 64 * 1024;
|
||||
export const DEFAULT_PIPE_OUTPUT_DRAIN_MS = 2_000;
|
||||
export const SHELL_RUN_RESOURCE_PREFIX = 'maka://runtime/background-tasks';
|
||||
export const MAX_SHELL_RUN_RESOURCE_REF_CHARS =
|
||||
SHELL_RUN_RESOURCE_PREFIX.length + 1 + SHELL_RUN_ID_MAX_CHARS;
|
||||
|
||||
export { isWellFormedTerminalInput };
|
||||
|
||||
const SHELL_RUN_RESOURCE_PATH_PATTERN = /^\/background-tasks\/([^/]+)$/;
|
||||
|
||||
type ShellRunToolResult = Extract<ToolResultContent, { kind: 'shell_run' }>;
|
||||
|
||||
export class ShellRunPtyControlClosedError extends Error {
|
||||
constructor() {
|
||||
super('This PTY is stopping and no longer accepts input; use Read to observe its final state');
|
||||
this.name = 'ShellRunPtyControlClosedError';
|
||||
}
|
||||
}
|
||||
|
||||
export interface ShellRunProcessManagerInput {
|
||||
store: ShellRunStore;
|
||||
newId: () => string;
|
||||
now: () => number;
|
||||
onShellRunUpdate?: (update: ShellRunUpdate) => void;
|
||||
onPtyData?: (event: ShellRunPtyDataEvent) => void;
|
||||
maxLiveShellRuns?: number;
|
||||
maxLivePtyRuns?: number;
|
||||
flushIntervalMs?: number;
|
||||
flushBytes?: number;
|
||||
maxRetainedChars?: number;
|
||||
maxLiveEmitChars?: number;
|
||||
killGraceMs?: number;
|
||||
exitAcknowledgementMs?: number;
|
||||
pipeOutputDrainMs?: number;
|
||||
/** Schedules the automatic durable flush and returns its canceler; injected so tests can drive flush timing. */
|
||||
scheduleFlush?: (run: () => void, delayMs: number) => () => void;
|
||||
/** Schedules the run timeout and returns its canceler; injected so tests can drive timeout timing. */
|
||||
scheduleTimeout?: (run: () => void, delayMs: number) => () => void;
|
||||
}
|
||||
|
||||
export interface ShellRunBashInput {
|
||||
sessionId: string;
|
||||
sourceRunId?: string;
|
||||
sourceTurnId: string;
|
||||
sourceToolCallId: string;
|
||||
/** User-owned terminals stay outside model context summaries. */
|
||||
visibility?: 'model' | 'user';
|
||||
cwd: string;
|
||||
command: string;
|
||||
/** Final executable argv. When present, bypasses host-shell parsing. */
|
||||
argv?: readonly string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
/** Binary payloads exposed to pipe-mode children on inherited descriptors. */
|
||||
fdInputs?: readonly ChildFdInput[];
|
||||
pty?: boolean;
|
||||
timeoutMs?: number;
|
||||
abortSignal?: AbortSignal;
|
||||
emitOutput: (stream: 'stdout' | 'stderr', chunk: string) => void;
|
||||
shell?: ShellPlan;
|
||||
/** Effective command sandbox selected before process launch. */
|
||||
sandboxType?: SandboxType;
|
||||
/** Invoked exactly once after startup failure or terminal process completion. */
|
||||
onCompletion?: (outcome: { successful: boolean }) => void;
|
||||
}
|
||||
|
||||
export interface ShellRunWriteInput {
|
||||
sessionId: string;
|
||||
ref: string;
|
||||
input?: string;
|
||||
actions?: readonly TerminalInputAction[];
|
||||
size?: { cols: number; rows: number };
|
||||
abortSignal?: AbortSignal;
|
||||
/** Client control may reach user-owned resources; model tools may not. */
|
||||
caller?: 'model' | 'client';
|
||||
}
|
||||
|
||||
export interface ShellRunPtyDataEvent {
|
||||
sessionId: string;
|
||||
ref: string;
|
||||
sequence: number;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface ShellRunPtySnapshot {
|
||||
sessionId: string;
|
||||
ref: string;
|
||||
sequence: number;
|
||||
buffer: string;
|
||||
size: { cols: number; rows: number };
|
||||
}
|
||||
|
||||
export interface RuntimeResourceReader {
|
||||
readRuntimeResource(
|
||||
sessionId: string,
|
||||
ref: string,
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<ToolResultContent>;
|
||||
}
|
||||
|
||||
export interface BackgroundTaskStopper {
|
||||
stopBackgroundTask(
|
||||
sessionId: string,
|
||||
ref: string,
|
||||
abortSignal: AbortSignal,
|
||||
caller?: 'model' | 'client',
|
||||
): Promise<ToolResultContent>;
|
||||
}
|
||||
|
||||
export interface PtyControlWriter {
|
||||
writeStdin(input: ShellRunWriteInput): Promise<ShellRunToolResult>;
|
||||
}
|
||||
|
||||
export function validateWriteStdinInput(input: ShellRunWriteInput): void {
|
||||
if (input.input !== undefined && input.actions !== undefined) {
|
||||
throw new Error('WriteStdin raw input and terminal actions are mutually exclusive');
|
||||
}
|
||||
if (input.input === undefined && input.actions === undefined && input.size === undefined) {
|
||||
throw new Error('WriteStdin requires input, actions, and/or size');
|
||||
}
|
||||
if (input.input !== undefined) {
|
||||
if (input.input.length === 0) throw new Error('WriteStdin input must not be empty');
|
||||
if (!isWellFormedTerminalInput(input.input))
|
||||
throw new Error('WriteStdin input must be well-formed Unicode');
|
||||
const bytes = Buffer.byteLength(input.input, 'utf8');
|
||||
if (bytes > MAX_WRITE_STDIN_INPUT_BYTES) {
|
||||
throw new Error(`WriteStdin input exceeds the ${MAX_WRITE_STDIN_INPUT_BYTES}-byte limit`);
|
||||
}
|
||||
}
|
||||
if (input.actions !== undefined) validateTerminalInputActions(input.actions);
|
||||
if (input.size) {
|
||||
if (
|
||||
!Number.isInteger(input.size.cols) ||
|
||||
input.size.cols < MIN_PTY_COLS ||
|
||||
input.size.cols > MAX_PTY_COLS
|
||||
) {
|
||||
throw new Error(`WriteStdin cols must be between ${MIN_PTY_COLS} and ${MAX_PTY_COLS}`);
|
||||
}
|
||||
if (
|
||||
!Number.isInteger(input.size.rows) ||
|
||||
input.size.rows < MIN_PTY_ROWS ||
|
||||
input.size.rows > MAX_PTY_ROWS
|
||||
) {
|
||||
throw new Error(`WriteStdin rows must be between ${MIN_PTY_ROWS} and ${MAX_PTY_ROWS}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateTerminalInputActions(actions: readonly TerminalInputAction[]): void {
|
||||
if (actions.length === 0) throw new Error('WriteStdin actions must not be empty');
|
||||
if (actions.length > MAX_WRITE_STDIN_ACTIONS) {
|
||||
throw new Error(`WriteStdin actions must not exceed ${MAX_WRITE_STDIN_ACTIONS} entries`);
|
||||
}
|
||||
for (const action of actions) {
|
||||
parseTerminalInputAction(action);
|
||||
}
|
||||
if (encodedTerminalInputActionsByteLength(actions) > MAX_WRITE_STDIN_INPUT_BYTES) {
|
||||
throw new Error(`WriteStdin actions exceed the ${MAX_WRITE_STDIN_INPUT_BYTES}-byte limit`);
|
||||
}
|
||||
}
|
||||
|
||||
export function shellRunResourceRef(shellRunId: string): string {
|
||||
if (!isShellRunId(shellRunId)) throw new Error('Invalid shell run id');
|
||||
return `${SHELL_RUN_RESOURCE_PREFIX}/${encodeURIComponent(shellRunId)}`;
|
||||
}
|
||||
|
||||
export function isShellRunResourceRef(ref: string): boolean {
|
||||
return parseShellRunResourceRef(ref) !== null;
|
||||
}
|
||||
|
||||
export function parseShellRunResourceRef(ref: string): { shellRunId: string } | null {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(ref);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
url.protocol !== 'maka:' ||
|
||||
url.hostname !== 'runtime' ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
url.port ||
|
||||
url.search ||
|
||||
url.hash
|
||||
)
|
||||
return null;
|
||||
const match = SHELL_RUN_RESOURCE_PATH_PATTERN.exec(url.pathname);
|
||||
if (!match) return null;
|
||||
const encodedId = match[1];
|
||||
if (!encodedId) return null;
|
||||
try {
|
||||
const shellRunId = decodeURIComponent(encodedId);
|
||||
if (!isShellRunId(shellRunId) || ref !== shellRunResourceRef(shellRunId)) return null;
|
||||
return { shellRunId };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user