@openape/apes 0.6.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,9 +35,12 @@ apes grants list
35
35
 
36
36
  ## ape-shell: Grant-Secured Shell Wrapper
37
37
 
38
- `ape-shell` is a drop-in shell replacement that routes every command through a DDISA grant. Useful for sandboxing AI coding agents (OpenClaw, Claude Code, etc.) so they can only execute pre-approved commands.
38
+ `ape-shell` is a drop-in shell that routes every command through a DDISA grant. It has two modes:
39
39
 
40
- ### How it works
40
+ 1. **One-shot mode** (`ape-shell -c "<command>"`) — the historical wrapper. Runs a single command through the grant flow and exits. Used by `$SHELL -c` patterns (e.g. `openclaw tui`, `xargs`, git hooks, sshd non-interactive sessions, etc.).
41
+ 2. **Interactive mode** (`ape-shell` with no args, or as a login shell) — a full interactive REPL with a persistent bash backend. Every line the user types is routed through the grant flow **before** bash sees it, and executed in bash's persistent state (so `cd`, `export`, aliases, functions, pipes, TUI apps like `vim`/`less`/`top` all work natively).
42
+
43
+ ### How the one-shot mode works
41
44
 
42
45
  ```
43
46
  $SHELL -c "git status"
@@ -51,16 +54,50 @@ apes run --shell -- bash -c "git status"
51
54
  3. No grant → request + wait for human approval → execute
52
55
  ```
53
56
 
54
- ### Setup for an AI agent session
57
+ ### How the interactive mode works
58
+
59
+ ```
60
+ ape-shell
61
+
62
+ ┌─ PROMPT ─────────────────────────────────────────┐
63
+ │ apes$ <user types here> │
64
+ │ → multi-line detection via `bash -n` dry-parse │
65
+ │ → grant dispatch (adapter or ape-shell session)│
66
+ │ → on approval: write line to persistent bash │
67
+ │ → stream output, detect prompt marker │
68
+ └──────────────────────────────────────────────────┘
69
+ ```
70
+
71
+ Every line is audited in `~/.config/apes/audit.jsonl` (session id, line, grant id, exit code).
72
+
73
+ ### Setup for an AI agent session (one-shot mode)
55
74
 
56
75
  ```bash
57
- # Point the agent's SHELL at ape-shell
58
- SHELL=$(which ape-shell) openclaw
76
+ # Point the agent's SHELL at ape-shell — each spawned command
77
+ # goes through the one-shot grant flow.
78
+ SHELL=$(which ape-shell) openclaw tui
59
79
  ```
60
80
 
61
81
  The first command requests a session grant. After the human approves it (with `grant_type: timed, duration: 8h`), all subsequent commands reuse the same grant without interaction.
62
82
 
63
- ### Example
83
+ ### Setup as a login shell (interactive mode)
84
+
85
+ ```bash
86
+ # 1. Register ape-shell as a valid login shell (once per host)
87
+ echo "$(which ape-shell)" | sudo tee -a /etc/shells
88
+
89
+ # 2. Set it as the login shell for a user (e.g. openclaw)
90
+ sudo chsh -s "$(which ape-shell)" openclaw
91
+ ```
92
+
93
+ After this:
94
+
95
+ - `ssh openclaw@host` — sshd starts ape-shell as an interactive REPL (sshd passes the login shell with a `-` prefix on argv[0], which ape-shell detects)
96
+ - `ssh openclaw@host "ls"` — sshd invokes `ape-shell -c "ls"`, which still flows through the **one-shot** path (no regression)
97
+ - `su - openclaw` — drops into the interactive REPL
98
+ - Terminal / console login — same as SSH interactive
99
+
100
+ ### Example (one-shot)
64
101
 
65
102
  ```bash
66
103
  $ apes login
@@ -78,6 +115,31 @@ def456 Previous commit
78
115
  ...
79
116
  ```
80
117
 
118
+ ### Example (interactive)
119
+
120
+ ```bash
121
+ $ ape-shell
122
+ apes interactive shell
123
+ Ctrl-D to exit.
124
+
125
+ apes$ cd /tmp
126
+ # (grant approved, reused for free)
127
+
128
+ apes$ ls
129
+ # structured adapter grant for `ls` → approve → output
130
+
131
+ apes$ for i in 1 2 3; do
132
+ > echo $i
133
+ > done
134
+ # single grant for the whole compound, bash runs it natively
135
+
136
+ apes$ vim notes.md
137
+ # grant approved → full TUI vim, raw-mode passthrough
138
+
139
+ apes$ ^D
140
+ Goodbye.
141
+ ```
142
+
81
143
  ## Commands
82
144
 
83
145
  ### Authentication
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CONFIG_DIR
4
+ } from "./chunk-TBYYREL6.js";
5
+
6
+ // src/auth-lock.ts
7
+ import { open, rm, stat } from "fs/promises";
8
+ import { join } from "path";
9
+ var LOCK_FILE = join(CONFIG_DIR, "auth.json.lock");
10
+ async function acquireAuthLock(opts = {}) {
11
+ const deadline = Date.now() + (opts.timeoutMs ?? 5e3);
12
+ while (Date.now() < deadline) {
13
+ try {
14
+ const handle = await open(LOCK_FILE, "wx");
15
+ return { handle };
16
+ } catch (err) {
17
+ if (err.code !== "EEXIST")
18
+ throw err;
19
+ try {
20
+ const s = await stat(LOCK_FILE);
21
+ if (Date.now() - s.mtimeMs > 3e4)
22
+ await rm(LOCK_FILE, { force: true });
23
+ } catch {
24
+ }
25
+ await new Promise((r) => setTimeout(r, 100));
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ async function releaseAuthLock(lock) {
31
+ try {
32
+ await lock.handle.close();
33
+ } finally {
34
+ await rm(LOCK_FILE, { force: true });
35
+ }
36
+ }
37
+ export {
38
+ acquireAuthLock,
39
+ releaseAuthLock
40
+ };
41
+ //# sourceMappingURL=auth-lock-SRUFWJC3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/auth-lock.ts"],"sourcesContent":["import type { FileHandle } from 'node:fs/promises'\nimport { open, rm, stat } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { CONFIG_DIR } from './config'\n\nconst LOCK_FILE = join(CONFIG_DIR, 'auth.json.lock')\n\nexport interface AuthLock {\n handle: FileHandle\n}\n\n/**\n * Best-effort exclusive file lock to serialize concurrent token refreshes\n * between parallel apes / ape-shell invocations. Uses O_CREAT|O_EXCL which\n * is atomic on POSIX. Returns null on timeout so the caller can fall back\n * to \"just re-read auth.json\" (the assumption being that another process\n * successfully refreshed in the meantime).\n *\n * A stale lock older than 30s is considered abandoned (from a crashed\n * process) and is removed so the next acquire can proceed.\n */\nexport async function acquireAuthLock(\n opts: { timeoutMs?: number } = {},\n): Promise<AuthLock | null> {\n const deadline = Date.now() + (opts.timeoutMs ?? 5000)\n while (Date.now() < deadline) {\n try {\n const handle = await open(LOCK_FILE, 'wx')\n return { handle }\n }\n catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST')\n throw err\n // Stale lock? If the file is older than 30s, remove it and retry.\n try {\n const s = await stat(LOCK_FILE)\n if (Date.now() - s.mtimeMs > 30_000)\n await rm(LOCK_FILE, { force: true })\n }\n catch {\n // File gone between stat and rm — next iteration will retry the open.\n }\n await new Promise(r => setTimeout(r, 100))\n }\n }\n return null\n}\n\nexport async function releaseAuthLock(lock: AuthLock): Promise<void> {\n try {\n await lock.handle.close()\n }\n finally {\n await rm(LOCK_FILE, { force: true })\n }\n}\n"],"mappings":";;;;;;AACA,SAAS,MAAM,IAAI,YAAY;AAC/B,SAAS,YAAY;AAGrB,IAAM,YAAY,KAAK,YAAY,gBAAgB;AAgBnD,eAAsB,gBACpB,OAA+B,CAAC,GACN;AAC1B,QAAM,WAAW,KAAK,IAAI,KAAK,KAAK,aAAa;AACjD,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,WAAW,IAAI;AACzC,aAAO,EAAE,OAAO;AAAA,IAClB,SACO,KAAK;AACV,UAAK,IAA8B,SAAS;AAC1C,cAAM;AAER,UAAI;AACF,cAAM,IAAI,MAAM,KAAK,SAAS;AAC9B,YAAI,KAAK,IAAI,IAAI,EAAE,UAAU;AAC3B,gBAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AAAA,MACvC,QACM;AAAA,MAEN;AACA,YAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,GAAG,CAAC;AAAA,IAC3C;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,gBAAgB,MAA+B;AACnE,MAAI;AACF,UAAM,KAAK,OAAO,MAAM;AAAA,EAC1B,UACA;AACE,UAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AAAA,EACrC;AACF;","names":[]}