81c3b1b44a
Adds `aish -p "<text>"` for Unix-pipeline composability:
tail app.log | aish -p "any anomalies?"
aish -p "summarize: $(curl -sS https://...)"
The flag bypasses repl.lua entirely. On invocation:
1. Stdin: when not a TTY, read to EOF and prepend to the prompt as a
fenced block. ffi.libc.isatty(0) gates the read so interactive
`aish -p "..."` (no pipe) doesn't hang.
2. Resolve config.models[config.default_model].
3. Stream broker.chat_stream replies to stdout; finalize with newline.
4. Exit 0 on success, 1 on broker error, 2 on arg / config error.
Behavior NOT in -p mode (kept simple per the issue's "no repl.lua
involvement"):
- No MCP, no tool loop, no Norris, no routing, no memory injection.
- "CMD:" lines in the reply are printed verbatim, NOT executed —
callers can grep / pipe them as they wish.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
187 lines
6.4 KiB
Lua
187 lines
6.4 KiB
Lua
-- ffi/libc.lua — shared libc bindings.
|
|
-- Phase 0: chdir, errno, strerror — enough for `cd` interception in executor.
|
|
-- Phase 1: waitpid + WEXITSTATUS, raw fd I/O (read/write/close), kill — the
|
|
-- syscalls ffi/pty needs to drive a forkpty'd child.
|
|
-- See docs/PHASE0.md §7 and docs/PHASE1.md §3.
|
|
|
|
local ffi = require("ffi")
|
|
local bit = require("bit")
|
|
|
|
ffi.cdef[[
|
|
int chdir(const char *path);
|
|
int *__errno_location(void);
|
|
char *strerror(int errnum);
|
|
|
|
typedef int pid_t;
|
|
typedef long ssize_t;
|
|
typedef unsigned long size_t;
|
|
|
|
pid_t waitpid(pid_t pid, int *wstatus, int options);
|
|
ssize_t read (int fd, void *buf, size_t count);
|
|
ssize_t write (int fd, const void *buf, size_t count);
|
|
int close (int fd);
|
|
int kill (pid_t pid, int sig);
|
|
|
|
/* termios for raw-mode toggle around interactive PTY children. The struct
|
|
is treated as opaque — cfmakeraw fills it; size 64 is comfortably larger
|
|
than glibc's struct termios (60 bytes) on aarch64/x86_64 Linux. */
|
|
struct termios { char _opaque[64]; };
|
|
int tcgetattr(int fd, struct termios *tio);
|
|
int tcsetattr(int fd, int actions, const struct termios *tio);
|
|
void cfmakeraw(struct termios *tio);
|
|
|
|
/* poll for stdin↔master multiplex in executor. */
|
|
struct pollfd { int fd; short events; short revents; };
|
|
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
|
|
|
|
/* Phase 4: advisory file locking on memory.jsonl. Single-writer
|
|
enforcement via LOCK_EX | LOCK_NB — fail-fast if another aish
|
|
process holds the lock. */
|
|
int flock(int fd, int operation);
|
|
|
|
/* TTY detection for non-interactive mode (`aish -p`). Returns 1 if the
|
|
fd refers to a terminal, 0 otherwise (sets errno on error). */
|
|
int isatty(int fd);
|
|
]]
|
|
|
|
local C = ffi.C
|
|
|
|
local M = {}
|
|
|
|
-- ---------------------------------------------------------------- chdir / errno
|
|
-- Phase 0 invariants. Apply chdir per PHASE0.md §7.
|
|
-- Returns: true on success; false, errmsg on failure.
|
|
function M.chdir(path)
|
|
local rc = C.chdir(path)
|
|
if rc == 0 then return true end
|
|
return false, ffi.string(C.strerror(C.__errno_location()[0]))
|
|
end
|
|
|
|
function M.errno() return C.__errno_location()[0] end
|
|
function M.strerror(en) return ffi.string(C.strerror(en)) end
|
|
|
|
-- ---------------------------------------------------------------- waitpid
|
|
-- Mirrors glibc's WIFEXITED / WEXITSTATUS / WIFSIGNALED / WTERMSIG macros.
|
|
local function WIFEXITED (status) return bit.band(status, 0x7f) == 0 end
|
|
local function WEXITSTATUS(status) return bit.band(bit.rshift(status, 8), 0xff) end
|
|
local function WIFSIGNALED(status)
|
|
-- signal-killed iff low 7 bits in 1..126
|
|
local s = bit.band(status, 0x7f)
|
|
return s ~= 0 and s ~= 0x7f
|
|
end
|
|
local function WTERMSIG (status) return bit.band(status, 0x7f) end
|
|
|
|
M.WIFEXITED = WIFEXITED
|
|
M.WEXITSTATUS = WEXITSTATUS
|
|
M.WIFSIGNALED = WIFSIGNALED
|
|
M.WTERMSIG = WTERMSIG
|
|
|
|
-- waitpid wrapper. Returns (kind, value):
|
|
-- "exit", exit_code on normal exit (WIFEXITED -> WEXITSTATUS)
|
|
-- "signal", signum on signal kill (WIFSIGNALED -> WTERMSIG)
|
|
-- nil, errmsg on waitpid syscall failure
|
|
local status_buf = ffi.new("int[1]")
|
|
function M.waitpid(pid, options)
|
|
status_buf[0] = 0
|
|
local rc = C.waitpid(pid, status_buf, options or 0)
|
|
if rc < 0 then
|
|
return nil, ffi.string(C.strerror(C.__errno_location()[0]))
|
|
end
|
|
local status = status_buf[0]
|
|
if WIFEXITED(status) then return "exit", WEXITSTATUS(status) end
|
|
if WIFSIGNALED(status) then return "signal", WTERMSIG(status) end
|
|
return "other", status
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- raw fd I/O
|
|
-- Used by ffi/pty for master-fd transfer. Errors return nil + errmsg so
|
|
-- callers can decide between EAGAIN/EINTR retry and abort. EOF on read is
|
|
-- represented as ("", 0) — empty string, zero bytes.
|
|
-- Note: READ_BUF is module-shared. Phase 1 has no reentrant M.read callers
|
|
-- (no coroutines, no concurrent FFI callbacks performing reads); revisit if
|
|
-- that ever changes.
|
|
local READ_BUF = ffi.new("char[?]", 4096)
|
|
|
|
function M.read(fd, count)
|
|
count = count or 4096
|
|
local buf = (count <= 4096) and READ_BUF or ffi.new("char[?]", count)
|
|
local n = C.read(fd, buf, count)
|
|
if n < 0 then
|
|
return nil, ffi.string(C.strerror(C.__errno_location()[0])), M.errno()
|
|
end
|
|
return ffi.string(buf, n), tonumber(n)
|
|
end
|
|
|
|
function M.write(fd, data)
|
|
local n = C.write(fd, data, #data)
|
|
if n < 0 then
|
|
return nil, ffi.string(C.strerror(C.__errno_location()[0])), M.errno()
|
|
end
|
|
return tonumber(n)
|
|
end
|
|
|
|
function M.close(fd)
|
|
return C.close(fd) == 0
|
|
end
|
|
|
|
function M.kill(pid, sig)
|
|
local rc = C.kill(pid, sig)
|
|
if rc == 0 then return true end
|
|
return false, ffi.string(C.strerror(C.__errno_location()[0]))
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- termios
|
|
-- Save current tty mode and switch to raw via cfmakeraw. Returns the saved
|
|
-- termios pointer (to be passed back to M.restore_termios) or (nil, err) if
|
|
-- fd isn't a tty (e.g. stdin redirected from a file in CI / scripted runs).
|
|
local TCSANOW = 0
|
|
|
|
function M.set_raw(fd)
|
|
local saved = ffi.new("struct termios")
|
|
if C.tcgetattr(fd, saved) < 0 then
|
|
return nil, M.strerror(M.errno())
|
|
end
|
|
local raw = ffi.new("struct termios")
|
|
ffi.copy(raw, saved, ffi.sizeof("struct termios"))
|
|
C.cfmakeraw(raw)
|
|
if C.tcsetattr(fd, TCSANOW, raw) < 0 then
|
|
return nil, M.strerror(M.errno())
|
|
end
|
|
return saved
|
|
end
|
|
|
|
function M.restore_termios(fd, saved)
|
|
return C.tcsetattr(fd, TCSANOW, saved) == 0
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- poll
|
|
M.POLLIN = 0x0001
|
|
M.EINTR = 4
|
|
|
|
-- Returns: rc (>= 0 fds ready, 0 timeout, -1 error)
|
|
function M.poll(fds_arr, nfds, timeout_ms)
|
|
return C.poll(fds_arr, nfds, timeout_ms or -1)
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- flock
|
|
-- Advisory file locking. Phase 4 uses LOCK_EX | LOCK_NB so a second
|
|
-- aish process opening the same memory.jsonl fails fast rather than
|
|
-- blocking. Lock is released on fd close or process exit.
|
|
M.LOCK_EX = 2
|
|
M.LOCK_NB = 4
|
|
M.LOCK_UN = 8
|
|
|
|
-- Returns: true on success; false, errmsg on failure (e.g. EWOULDBLOCK
|
|
-- when LOCK_NB is set and another holder exists).
|
|
function M.flock(fd, op)
|
|
if C.flock(fd, op) == 0 then return true end
|
|
return false, ffi.string(C.strerror(C.__errno_location()[0]))
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- isatty
|
|
function M.isatty(fd)
|
|
return C.isatty(fd) == 1
|
|
end
|
|
|
|
return M
|