@oh-hai/cli 0.1.0-beta.0
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 +154 -0
- package/dist/auth/file-backend.d.ts +16 -0
- package/dist/auth/file-backend.js +98 -0
- package/dist/auth/file-backend.js.map +1 -0
- package/dist/auth/keychain.d.ts +54 -0
- package/dist/auth/keychain.js +232 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/resolve-token.d.ts +34 -0
- package/dist/auth/resolve-token.js +91 -0
- package/dist/auth/resolve-token.js.map +1 -0
- package/dist/auth/secure-write.d.ts +2 -0
- package/dist/auth/secure-write.js +30 -0
- package/dist/auth/secure-write.js.map +1 -0
- package/dist/auth/token-store.d.ts +104 -0
- package/dist/auth/token-store.js +208 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +238 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +370 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/ask.d.ts +2 -0
- package/dist/commands/ask.js +246 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/context.d.ts +72 -0
- package/dist/commands/context.js +7 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +237 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/flags.d.ts +25 -0
- package/dist/commands/flags.js +100 -0
- package/dist/commands/flags.js.map +1 -0
- package/dist/commands/handlers.d.ts +2 -0
- package/dist/commands/handlers.js +26 -0
- package/dist/commands/handlers.js.map +1 -0
- package/dist/commands/http.d.ts +8 -0
- package/dist/commands/http.js +19 -0
- package/dist/commands/http.js.map +1 -0
- package/dist/commands/inbox.d.ts +2 -0
- package/dist/commands/inbox.js +111 -0
- package/dist/commands/inbox.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +272 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/messaging/await.d.ts +43 -0
- package/dist/commands/messaging/await.js +125 -0
- package/dist/commands/messaging/await.js.map +1 -0
- package/dist/commands/messaging/build.d.ts +46 -0
- package/dist/commands/messaging/build.js +66 -0
- package/dist/commands/messaging/build.js.map +1 -0
- package/dist/commands/messaging/http.d.ts +22 -0
- package/dist/commands/messaging/http.js +270 -0
- package/dist/commands/messaging/http.js.map +1 -0
- package/dist/commands/messaging/identity.d.ts +29 -0
- package/dist/commands/messaging/identity.js +63 -0
- package/dist/commands/messaging/identity.js.map +1 -0
- package/dist/commands/messaging/shared.d.ts +53 -0
- package/dist/commands/messaging/shared.js +135 -0
- package/dist/commands/messaging/shared.js.map +1 -0
- package/dist/commands/messaging/state.d.ts +26 -0
- package/dist/commands/messaging/state.js +82 -0
- package/dist/commands/messaging/state.js.map +1 -0
- package/dist/commands/messaging/validate.d.ts +40 -0
- package/dist/commands/messaging/validate.js +193 -0
- package/dist/commands/messaging/validate.js.map +1 -0
- package/dist/commands/messaging/wire.d.ts +133 -0
- package/dist/commands/messaging/wire.js +16 -0
- package/dist/commands/messaging/wire.js.map +1 -0
- package/dist/commands/notify.d.ts +2 -0
- package/dist/commands/notify.js +68 -0
- package/dist/commands/notify.js.map +1 -0
- package/dist/commands/registry.d.ts +14 -0
- package/dist/commands/registry.js +144 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/stub.d.ts +1 -0
- package/dist/commands/stub.js +9 -0
- package/dist/commands/stub.js.map +1 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +223 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/commands/whoami.d.ts +6 -0
- package/dist/commands/whoami.js +90 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config-file.d.ts +38 -0
- package/dist/config-file.js +233 -0
- package/dist/config-file.js.map +1 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/envelope.d.ts +25 -0
- package/dist/envelope.js +41 -0
- package/dist/envelope.js.map +1 -0
- package/dist/exit-codes.d.ts +51 -0
- package/dist/exit-codes.js +57 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/help.d.ts +1 -0
- package/dist/help.js +17 -0
- package/dist/help.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.js +18 -0
- package/dist/terminal.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# `@oh-hai/cli` — the `oh-hai` command-line tool
|
|
2
|
+
|
|
3
|
+
One universal, runtime-agnostic way for an autonomous agent to reach a human through an
|
|
4
|
+
MA2H Hub: `oh-hai notify | ask | task`. Shell-out is the lowest common denominator (Claude
|
|
5
|
+
skills are Claude-only, MCP is partial); a small, well-behaved binary works everywhere.
|
|
6
|
+
|
|
7
|
+
The design contract is [`docs/specs/cli.md`](../docs/specs/cli.md) (the command surface,
|
|
8
|
+
auth/token model, config resolution, exit codes, and the `--json` envelope). This package
|
|
9
|
+
implements that contract.
|
|
10
|
+
|
|
11
|
+
> **The one invariant:** the agent bearer token **never passes through the LLM**. `oh-hai
|
|
12
|
+
> login` puts the token in the OS keychain; the agent invokes `oh-hai` **by reference** and
|
|
13
|
+
> never sees, prints, or reasons over the secret ([spec §5](../docs/specs/cli.md)).
|
|
14
|
+
|
|
15
|
+
## Status
|
|
16
|
+
|
|
17
|
+
#105 shipped **package placement + the machine contract + the publish pipeline + docs**;
|
|
18
|
+
#106 wired the **auth commands** (token storage + login/logout/whoami); #107 wired the
|
|
19
|
+
**messaging commands** (notify/ask/task). The remaining command *bodies* land in downstream
|
|
20
|
+
issues:
|
|
21
|
+
|
|
22
|
+
| Works today | Lands in |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `--help`, `--version` (and `--version --json`) | — |
|
|
25
|
+
| Global-flag parsing, command dispatch | — |
|
|
26
|
+
| Exit-code table (§7), `--json` envelope (§8), config resolution (§6) | — |
|
|
27
|
+
| `login --token-stdin` / `logout` / `whoami` (OS keychain + `0600` file fallback) | ✅ #106 |
|
|
28
|
+
| `oh-hai login` **device-code** acquisition (server device-authz + web approval) | **#166** |
|
|
29
|
+
| `notify` / `ask submit\|await` / `task submit\|await` (account-aware; `--json`; pull-await) | ✅ #107 |
|
|
30
|
+
| `agents list\|create\|revoke` | later |
|
|
31
|
+
| `doctor` | **#108** |
|
|
32
|
+
|
|
33
|
+
Every not-yet-implemented command exits **1** with a stable `not_implemented` error and a
|
|
34
|
+
pointer to its owning issue — it never silently no-ops or fakes success.
|
|
35
|
+
|
|
36
|
+
### Auth (`login` / `logout` / `whoami`)
|
|
37
|
+
|
|
38
|
+
The agent bearer token is stored in the **OS keychain** (macOS Keychain, Linux Secret
|
|
39
|
+
Service) or, where no keychain is available, a **`0600` file** at `~/.config/oh-hai/credentials`
|
|
40
|
+
(with a loud one-time warning). It is keyed per Hub (`<origin>|<agent id>`), **never printed**,
|
|
41
|
+
and **never** accepted as a flag value ([spec §5](../docs/specs/cli.md)).
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Store a token minted in the web app / via `oh-hai agents create` (never typed, never logged):
|
|
45
|
+
printf %s "$AGENT_TOKEN" | oh-hai login --account my-agent --token-stdin
|
|
46
|
+
|
|
47
|
+
oh-hai whoami # agent id + base URL + token presence (…last4) + source
|
|
48
|
+
oh-hai whoami --check # also probe the Hub for token validity
|
|
49
|
+
oh-hai logout # remove this identity's token (--all clears every identity)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Bare `oh-hai login` (the browser **device-code** flow) is not available yet — it needs a
|
|
53
|
+
server device-authorization grant + a web approval page (tracked in **#166**); until then it
|
|
54
|
+
declines with a pointer to `--token-stdin`.
|
|
55
|
+
|
|
56
|
+
### Messaging (`notify` / `ask` / `task`)
|
|
57
|
+
|
|
58
|
+
The productized counterparts of the `@oh-hai/scripts` wrappers ([spec §4.4](../docs/specs/cli.md)):
|
|
59
|
+
the bearer is resolved **account-aware** (keychain / `0600` file / `MA2H_AGENT_TOKEN` env — not
|
|
60
|
+
raw env), output follows the `--json` envelope, and Hub statuses map to the §7 exit codes.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
oh-hai notify --title "Build finished" --body "All green" --priority normal
|
|
64
|
+
oh-hai ask submit --mode select --option ship:Ship --option hold:Hold \
|
|
65
|
+
--resolver human:owner --title "Ship v1?"
|
|
66
|
+
oh-hai ask await --id msg_02A… # poll until the human answers; prints the outcome
|
|
67
|
+
oh-hai task submit --instructions "Rotate the signing key" --title "Rotate key" \
|
|
68
|
+
--resolver human:owner # terminal by default (--callback opts into a return leg)
|
|
69
|
+
oh-hai <ask|task> submit --dry-run … # compose + print the envelope, POST nothing
|
|
70
|
+
oh-hai ask submit --envelope @captured.json # idempotent replay of a captured envelope (§4.7)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **`--state` (sealed resume) is not yet available in the CLI.** The AES-256-GCM state seal is
|
|
74
|
+
> vendored crypto that the self-contained published bin can't import; sealing needs the shared
|
|
75
|
+
> MA2H core extracted first (follow-up). Passing `--state` is rejected with a pointer; use the
|
|
76
|
+
> `@oh-hai/scripts` wrappers for sealed state until then.
|
|
77
|
+
|
|
78
|
+
## Install
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# npm (global)
|
|
82
|
+
npm install -g @oh-hai/cli
|
|
83
|
+
|
|
84
|
+
# Homebrew (tap)
|
|
85
|
+
brew install autnmy/tap/oh-hai
|
|
86
|
+
|
|
87
|
+
# curl installer (served at ohhai.app once DNS lands — issue #99)
|
|
88
|
+
curl -fsSL https://ohhai.app/install | sh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Command surface (see [spec §4](../docs/specs/cli.md))
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
oh-hai [global flags] <command> [subcommand] [flags]
|
|
95
|
+
|
|
96
|
+
Auth login | logout | whoami
|
|
97
|
+
Messaging notify | ask | task
|
|
98
|
+
Agents agents list | create | revoke
|
|
99
|
+
Health doctor
|
|
100
|
+
|
|
101
|
+
Global flags: --base-url --account --json --quiet --verbose --no-color --timeout
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Usage examples
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
$ oh-hai --version
|
|
108
|
+
0.0.0
|
|
109
|
+
|
|
110
|
+
$ oh-hai --version --json
|
|
111
|
+
{"ok":true,"command":"version","data":{"version":"0.0.0","wire_version":"v0.3"},"error":null}
|
|
112
|
+
|
|
113
|
+
$ oh-hai notify --title "Build finished" --body "All green" --priority normal
|
|
114
|
+
$ oh-hai ask submit --mode select --option ship:Ship --option hold:Hold \
|
|
115
|
+
--resolver human:owner --title "Ship v1?" --json
|
|
116
|
+
$ oh-hai ask await --id msg_02A… --json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Machine contract
|
|
120
|
+
|
|
121
|
+
- **Exit codes (§7):** `0` success · `1` generic · `2` usage · `3` auth · `4` not-found ·
|
|
122
|
+
`5` network · `6` server · `7` timeout · `8` conflict · `9` invalid-request. (The scaffold
|
|
123
|
+
adds a `not_implemented` error string that maps to exit `1`.)
|
|
124
|
+
- **`--json` envelope (§8):** every command emits one uniform shape:
|
|
125
|
+
```json
|
|
126
|
+
{ "ok": true, "command": "ask.submit", "data": { "id": "…" }, "error": null }
|
|
127
|
+
```
|
|
128
|
+
Absent optional fields are **omitted** from `data`, not set to `null`. On failure `ok` is
|
|
129
|
+
`false`, `data` is `null`, and `error` is `{ code, message }`. The token is **never** in
|
|
130
|
+
the envelope ([spec §5.4](../docs/specs/cli.md)).
|
|
131
|
+
- **Config precedence (§6):** `flag > env > config file > default`, resolved per value. The
|
|
132
|
+
token is special-cased (keychain / `MA2H_AGENT_TOKEN` env — never a config value), and
|
|
133
|
+
per-project config is credential-safe: it may set only `output` / `timeout` / `color`, never
|
|
134
|
+
`base_url` / `account`.
|
|
135
|
+
|
|
136
|
+
## Develop
|
|
137
|
+
|
|
138
|
+
Node ≥ 24, run from the repo root:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pnpm --filter @oh-hai/cli start --help # run the CLI from source via tsx (or: pnpm oh-hai --help)
|
|
142
|
+
pnpm --filter @oh-hai/cli typecheck # tsc --noEmit
|
|
143
|
+
pnpm --filter @oh-hai/cli test # node:test unit suite
|
|
144
|
+
pnpm --filter @oh-hai/cli build # publish-time compile → dist/ (gitignored)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Dev + test run the `.ts` sources directly via `tsx` (repo convention — no build step). The
|
|
148
|
+
**published** artifact is compiled to `dist/` because end users have `node`, not `tsx`.
|
|
149
|
+
|
|
150
|
+
## Release
|
|
151
|
+
|
|
152
|
+
Tag-driven: push `cli-vX.Y.Z` (matching `package.json`) and the
|
|
153
|
+
[`cli-release`](../.github/workflows/cli-release.yml) workflow builds + publishes to npm.
|
|
154
|
+
See the runbook: [`docs/runbooks/cli-release.md`](../docs/runbooks/cli-release.md).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SecretBackend } from "./token-store.js";
|
|
2
|
+
/** Resolve the credentials file path, honoring `$XDG_CONFIG_HOME` then `~/.config` (§5.2).
|
|
3
|
+
* `env`/`home` are injectable so tests point at a temp dir instead of the real HOME. */
|
|
4
|
+
export declare function credentialsPath(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
5
|
+
/** The file-backed `SecretBackend`. Construct with an explicit path (tests) or let it resolve
|
|
6
|
+
* the XDG path. All ops are synchronous fs calls wrapped in the async `SecretBackend` shape. */
|
|
7
|
+
export declare class FileBackend implements SecretBackend {
|
|
8
|
+
readonly name: "file";
|
|
9
|
+
private readonly path;
|
|
10
|
+
constructor(path?: string);
|
|
11
|
+
get(key: string): Promise<string | undefined>;
|
|
12
|
+
set(key: string, token: string): Promise<void>;
|
|
13
|
+
delete(key: string): Promise<boolean>;
|
|
14
|
+
listKeys(): Promise<string[]>;
|
|
15
|
+
clearAll(): Promise<number>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The `0600` file-fallback credential backend (docs/specs/cli.md §5.2). Used precisely when
|
|
2
|
+
// the OS keychain is unavailable or locked (headless Linux without a Secret Service, a locked
|
|
3
|
+
// login keychain on an unattended session, minimal containers). The token is stored on disk
|
|
4
|
+
// UNENCRYPTED — the store emits a loud one-time warning when it falls back here (§5.2). The
|
|
5
|
+
// file lives at `~/.config/oh-hai/credentials` (honoring `$XDG_CONFIG_HOME`) with permissions
|
|
6
|
+
// `0600` and a `0700` parent dir.
|
|
7
|
+
//
|
|
8
|
+
// On-disk format: a single JSON object mapping the Hub-scoped key `<origin>|<agent id>` (§5.1)
|
|
9
|
+
// to the raw token. Keys never contain a token value; only this map's *values* are secrets, so
|
|
10
|
+
// enumeration (`listKeys`) is redaction-safe. The map is small (one entry per logged-in agent).
|
|
11
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { writeSecureFile } from "./secure-write.js";
|
|
15
|
+
/** Resolve the credentials file path, honoring `$XDG_CONFIG_HOME` then `~/.config` (§5.2).
|
|
16
|
+
* `env`/`home` are injectable so tests point at a temp dir instead of the real HOME. */
|
|
17
|
+
export function credentialsPath(env = process.env, home = homedir()) {
|
|
18
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
19
|
+
const base = xdg !== undefined && xdg !== "" ? xdg : join(home, ".config");
|
|
20
|
+
return join(base, "oh-hai", "credentials");
|
|
21
|
+
}
|
|
22
|
+
/** Read + parse the credentials map. A missing file (or unreadable/corrupt JSON) is treated as
|
|
23
|
+
* an empty store — the store must never throw just because nothing has been written yet. */
|
|
24
|
+
function readMap(path) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = readFileSync(path, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
// Keep only string values — a hand-edited file with a non-string value can't be a token.
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
40
|
+
if (typeof value === "string") {
|
|
41
|
+
out[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Write the map atomically at `0600` (parent dir `0700`) via `writeSecureFile` — temp file +
|
|
51
|
+
* rename, so there is no window where a pre-existing looser file exposes the new secret and no
|
|
52
|
+
* partial file survives a crash. */
|
|
53
|
+
function writeMap(path, map) {
|
|
54
|
+
writeSecureFile(path, JSON.stringify(map));
|
|
55
|
+
}
|
|
56
|
+
/** The file-backed `SecretBackend`. Construct with an explicit path (tests) or let it resolve
|
|
57
|
+
* the XDG path. All ops are synchronous fs calls wrapped in the async `SecretBackend` shape. */
|
|
58
|
+
export class FileBackend {
|
|
59
|
+
name = "file";
|
|
60
|
+
path;
|
|
61
|
+
constructor(path = credentialsPath()) {
|
|
62
|
+
this.path = path;
|
|
63
|
+
}
|
|
64
|
+
get(key) {
|
|
65
|
+
return Promise.resolve(readMap(this.path)[key]);
|
|
66
|
+
}
|
|
67
|
+
set(key, token) {
|
|
68
|
+
const map = readMap(this.path);
|
|
69
|
+
map[key] = token;
|
|
70
|
+
writeMap(this.path, map);
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
}
|
|
73
|
+
delete(key) {
|
|
74
|
+
const map = readMap(this.path);
|
|
75
|
+
if (!(key in map)) {
|
|
76
|
+
return Promise.resolve(false);
|
|
77
|
+
}
|
|
78
|
+
delete map[key];
|
|
79
|
+
writeMap(this.path, map);
|
|
80
|
+
return Promise.resolve(true);
|
|
81
|
+
}
|
|
82
|
+
listKeys() {
|
|
83
|
+
return Promise.resolve(Object.keys(readMap(this.path)));
|
|
84
|
+
}
|
|
85
|
+
clearAll() {
|
|
86
|
+
const count = Object.keys(readMap(this.path)).length;
|
|
87
|
+
// Remove the file entirely rather than leaving an empty `{}` on disk.
|
|
88
|
+
try {
|
|
89
|
+
rmSync(this.path, { force: true });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// A best-effort scrub; if it can't be removed, overwrite with an empty map.
|
|
93
|
+
writeMap(this.path, {});
|
|
94
|
+
}
|
|
95
|
+
return Promise.resolve(count);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=file-backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-backend.js","sourceRoot":"","sources":["../../src/auth/file-backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { SecretBackend } from "./token-store.js";
|
|
2
|
+
/** The result of a single keychain operation, normalized across the platform CLIs. */
|
|
3
|
+
export interface KeychainRunResult {
|
|
4
|
+
/** `ok` — succeeded (read carries `value`); `not_found` — no such item; `unavailable` — the
|
|
5
|
+
* platform tool is missing / no Secret Service (→ file fallback); `error` — unexpected. */
|
|
6
|
+
status: "ok" | "not_found" | "unavailable" | "error";
|
|
7
|
+
/** The secret, present only for a successful `get`. */
|
|
8
|
+
value?: string;
|
|
9
|
+
/** A human-readable detail for `error` (never contains the token). */
|
|
10
|
+
message?: string;
|
|
11
|
+
}
|
|
12
|
+
/** A keychain operation. `token` is present only for `set`. */
|
|
13
|
+
export interface KeychainOp {
|
|
14
|
+
action: "get" | "set" | "delete";
|
|
15
|
+
key: string;
|
|
16
|
+
token?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Runs one keychain op. Injected into `KeychainBackend` so tests use an in-memory fake and
|
|
19
|
+
* never touch the real OS keychain. `signal` (used only by the availability probe) cancels a
|
|
20
|
+
* hung/locked-keychain spawn so it can't keep the CLI alive after the file fallback wins. */
|
|
21
|
+
export type KeychainRunner = (op: KeychainOp, signal?: AbortSignal) => Promise<KeychainRunResult>;
|
|
22
|
+
/** Low-level process result. `spawnError` is set (e.g. `"ENOENT"`) when the binary is missing. */
|
|
23
|
+
interface ProcResult {
|
|
24
|
+
code: number | null;
|
|
25
|
+
stdout: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
spawnError?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Spawn a process with an argv array (no shell), optionally writing `input` to stdin, and
|
|
30
|
+
* capture stdout/stderr. Never rejects — a spawn failure is reported via `spawnError`. An
|
|
31
|
+
* aborted `signal` kills the child so a hung probe can't outlive the CLI. */
|
|
32
|
+
export type ProcRunner = (file: string, args: string[], input?: string, signal?: AbortSignal) => Promise<ProcResult>;
|
|
33
|
+
/** Build the platform's system keychain runner. `platform`/`proc` are injectable for tests
|
|
34
|
+
* (assert argv/stdin per platform) — production uses `process.platform` and a real spawn. */
|
|
35
|
+
export declare function systemKeychainRunner(platform?: NodeJS.Platform, proc?: ProcRunner): KeychainRunner;
|
|
36
|
+
/** The keychain-backed `SecretBackend`. Delegates secret storage to the injected `runner` and
|
|
37
|
+
* maintains a keys-only index at `indexPath` for enumeration (`listKeys`/`clearAll`). */
|
|
38
|
+
export declare class KeychainBackend implements SecretBackend {
|
|
39
|
+
readonly name: "keychain";
|
|
40
|
+
private readonly runner;
|
|
41
|
+
private readonly indexPath;
|
|
42
|
+
constructor(runner: KeychainRunner, indexPath: string);
|
|
43
|
+
/** Side-effect-free availability probe: a `get` of a sentinel key, cancellable via `signal`.
|
|
44
|
+
* Only a clean response (`ok`/`not_found`) means the keychain is usable — `unavailable` (tool
|
|
45
|
+
* missing / no Secret Service) AND `error` (a locked keychain returning "user interaction is
|
|
46
|
+
* not allowed", etc.) both fall back to the `0600` file, which is the documented §5.2 path. */
|
|
47
|
+
available(signal?: AbortSignal): Promise<boolean>;
|
|
48
|
+
get(key: string): Promise<string | undefined>;
|
|
49
|
+
set(key: string, token: string): Promise<void>;
|
|
50
|
+
delete(key: string): Promise<boolean>;
|
|
51
|
+
listKeys(): Promise<string[]>;
|
|
52
|
+
clearAll(): Promise<number>;
|
|
53
|
+
}
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// The OS-keychain credential backend (docs/specs/cli.md §5.1). Zero-runtime-dependency by
|
|
2
|
+
// design (#105): instead of a native module like `keytar`, we shell out to the platform's
|
|
3
|
+
// own credential CLI through `node:child_process` —
|
|
4
|
+
// - macOS → `security …-generic-password` (Keychain Services)
|
|
5
|
+
// - Linux → `secret-tool` (libsecret / Secret Service: GNOME Keyring, KWallet)
|
|
6
|
+
// - other → reported UNAVAILABLE, so the store transparently uses the `0600` file fallback.
|
|
7
|
+
// (A Windows Credential Manager backend is a follow-up; until then Windows uses the file
|
|
8
|
+
// fallback, which is a valid headless setup per §5.2/§5.3.)
|
|
9
|
+
//
|
|
10
|
+
// The item is stored under service `oh-hai`, account = the Hub-scoped key `<origin>|<agent id>`
|
|
11
|
+
// (§5.1). The KEYCHAIN itself does not let us reliably enumerate our own items, so the backend
|
|
12
|
+
// maintains a small keys-only index file alongside (redaction-safe: it holds `<origin>|<agent
|
|
13
|
+
// id>` strings, NEVER tokens) so `logout --all` / `list` can enumerate.
|
|
14
|
+
//
|
|
15
|
+
// SECURITY: the token is passed on stdin where the CLI supports it (`secret-tool`). macOS
|
|
16
|
+
// `security add-generic-password` only accepts the password as an argv value (`-w <token>`),
|
|
17
|
+
// which is briefly visible in a process listing — an accepted, documented residual; we spawn
|
|
18
|
+
// with an argv array (no shell) so there is no additional shell-history/interpolation exposure.
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
21
|
+
import { writeSecureFile } from "./secure-write.js";
|
|
22
|
+
const SERVICE = "oh-hai";
|
|
23
|
+
const defaultProc = (file, args, input, signal) => new Promise((resolve) => {
|
|
24
|
+
if (signal?.aborted === true) {
|
|
25
|
+
resolve({ code: null, stdout: "", stderr: "", spawnError: "ABORTED" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const child = spawn(file, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
29
|
+
let stdout = "";
|
|
30
|
+
let stderr = "";
|
|
31
|
+
// Kill the child if the caller aborts (probe timeout) — SIGKILL because a locked keychain's
|
|
32
|
+
// `security` may ignore softer signals while blocked on a GUI prompt.
|
|
33
|
+
const onAbort = () => {
|
|
34
|
+
child.kill("SIGKILL");
|
|
35
|
+
};
|
|
36
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
37
|
+
child.stdout.on("data", (chunk) => (stdout += chunk.toString("utf8")));
|
|
38
|
+
child.stderr.on("data", (chunk) => (stderr += chunk.toString("utf8")));
|
|
39
|
+
child.on("error", (err) => {
|
|
40
|
+
signal?.removeEventListener("abort", onAbort);
|
|
41
|
+
resolve({ code: null, stdout, stderr, spawnError: err.code ?? err.message });
|
|
42
|
+
});
|
|
43
|
+
child.on("close", (code) => {
|
|
44
|
+
signal?.removeEventListener("abort", onAbort);
|
|
45
|
+
resolve({ code, stdout, stderr });
|
|
46
|
+
});
|
|
47
|
+
// A child that exits before consuming stdin (an immediate error) makes stdin emit EPIPE;
|
|
48
|
+
// without a listener that 'error' becomes an uncaught exception that crashes the CLI. We
|
|
49
|
+
// swallow it — the exit code / stderr already carry the failure via the 'close' handler.
|
|
50
|
+
child.stdin.on("error", () => { });
|
|
51
|
+
child.stdin.end(input ?? undefined);
|
|
52
|
+
});
|
|
53
|
+
/** A binary-missing spawn error means the platform's credential tool isn't installed → the
|
|
54
|
+
* store should fall back to the file backend rather than surface an error. */
|
|
55
|
+
function isMissingBinary(spawnError) {
|
|
56
|
+
return spawnError === "ENOENT" || spawnError === "EACCES";
|
|
57
|
+
}
|
|
58
|
+
/** On Linux, `secret-tool` present but with NO usable Secret Service (headless, no D-Bus, keyring
|
|
59
|
+
* not unlocked) exits non-zero just like a normal miss — but it's really "unavailable", and the
|
|
60
|
+
* store must fall back to the file rather than commit to a keychain that can't store anything.
|
|
61
|
+
* Detect the service-unavailable signatures in stderr. */
|
|
62
|
+
function isSecretServiceUnavailable(stderr) {
|
|
63
|
+
return /secret\s*service|dbus|d-bus|org\.freedesktop\.secrets|was not provided|not provided by any|cannot autolaunch|no such interface|no such object/i.test(stderr);
|
|
64
|
+
}
|
|
65
|
+
/** macOS `security` runner. Not-found is exit 44; a missing binary → unavailable. */
|
|
66
|
+
async function runMac(op, proc, signal) {
|
|
67
|
+
if (op.action === "get") {
|
|
68
|
+
const r = await proc("security", ["find-generic-password", "-s", SERVICE, "-a", op.key, "-w"], undefined, signal);
|
|
69
|
+
if (isMissingBinary(r.spawnError))
|
|
70
|
+
return { status: "unavailable" };
|
|
71
|
+
if (r.code === 0)
|
|
72
|
+
return { status: "ok", value: r.stdout.replace(/\n$/, "") };
|
|
73
|
+
if (r.code === 44)
|
|
74
|
+
return { status: "not_found" };
|
|
75
|
+
return { status: "error", message: `security find failed (code ${r.code ?? "?"})` };
|
|
76
|
+
}
|
|
77
|
+
if (op.action === "set") {
|
|
78
|
+
// `-U` updates in place if the item already exists (silent overwrite, §4.3).
|
|
79
|
+
const r = await proc("security", [
|
|
80
|
+
"add-generic-password", "-U", "-s", SERVICE, "-a", op.key, "-w", op.token ?? "",
|
|
81
|
+
]);
|
|
82
|
+
if (isMissingBinary(r.spawnError))
|
|
83
|
+
return { status: "unavailable" };
|
|
84
|
+
return r.code === 0 ? { status: "ok" } : { status: "error", message: `security add failed (code ${r.code ?? "?"})` };
|
|
85
|
+
}
|
|
86
|
+
const r = await proc("security", ["delete-generic-password", "-s", SERVICE, "-a", op.key]);
|
|
87
|
+
if (isMissingBinary(r.spawnError))
|
|
88
|
+
return { status: "unavailable" };
|
|
89
|
+
if (r.code === 0)
|
|
90
|
+
return { status: "ok" };
|
|
91
|
+
if (r.code === 44)
|
|
92
|
+
return { status: "not_found" };
|
|
93
|
+
return { status: "error", message: `security delete failed (code ${r.code ?? "?"})` };
|
|
94
|
+
}
|
|
95
|
+
/** Linux `secret-tool` runner. Token is written on stdin for `store` (no argv exposure). A
|
|
96
|
+
* non-zero exit whose stderr names a Secret Service / D-Bus failure is `unavailable` (→ file
|
|
97
|
+
* fallback), NOT a miss — otherwise a headless box with no keyring would commit to a keychain
|
|
98
|
+
* it can't actually write to. */
|
|
99
|
+
async function runLinux(op, proc, signal) {
|
|
100
|
+
if (op.action === "get") {
|
|
101
|
+
const r = await proc("secret-tool", ["lookup", "service", SERVICE, "account", op.key], undefined, signal);
|
|
102
|
+
if (isMissingBinary(r.spawnError))
|
|
103
|
+
return { status: "unavailable" };
|
|
104
|
+
if (r.code === 0 && r.stdout !== "")
|
|
105
|
+
return { status: "ok", value: r.stdout.replace(/\n$/, "") };
|
|
106
|
+
if (r.code !== 0 && isSecretServiceUnavailable(r.stderr))
|
|
107
|
+
return { status: "unavailable" };
|
|
108
|
+
// Otherwise secret-tool exits non-zero (or empty) when the item is simply absent.
|
|
109
|
+
return { status: "not_found" };
|
|
110
|
+
}
|
|
111
|
+
if (op.action === "set") {
|
|
112
|
+
const r = await proc("secret-tool", ["store", "--label", `oh-hai ${op.key}`, "service", SERVICE, "account", op.key], op.token ?? "");
|
|
113
|
+
if (isMissingBinary(r.spawnError))
|
|
114
|
+
return { status: "unavailable" };
|
|
115
|
+
if (r.code !== 0 && isSecretServiceUnavailable(r.stderr))
|
|
116
|
+
return { status: "unavailable" };
|
|
117
|
+
return r.code === 0 ? { status: "ok" } : { status: "error", message: `secret-tool store failed (code ${r.code ?? "?"})` };
|
|
118
|
+
}
|
|
119
|
+
const r = await proc("secret-tool", ["clear", "service", SERVICE, "account", op.key]);
|
|
120
|
+
if (isMissingBinary(r.spawnError))
|
|
121
|
+
return { status: "unavailable" };
|
|
122
|
+
if (r.code !== 0 && isSecretServiceUnavailable(r.stderr))
|
|
123
|
+
return { status: "unavailable" };
|
|
124
|
+
return r.code === 0 ? { status: "ok" } : { status: "not_found" };
|
|
125
|
+
}
|
|
126
|
+
/** Build the platform's system keychain runner. `platform`/`proc` are injectable for tests
|
|
127
|
+
* (assert argv/stdin per platform) — production uses `process.platform` and a real spawn. */
|
|
128
|
+
export function systemKeychainRunner(platform = process.platform, proc = defaultProc) {
|
|
129
|
+
if (platform === "darwin")
|
|
130
|
+
return (op, signal) => runMac(op, proc, signal);
|
|
131
|
+
if (platform === "linux")
|
|
132
|
+
return (op, signal) => runLinux(op, proc, signal);
|
|
133
|
+
// No supported credential CLI (Windows, etc.) → always unavailable so the store falls back.
|
|
134
|
+
return () => Promise.resolve({ status: "unavailable" });
|
|
135
|
+
}
|
|
136
|
+
// --- keys-only index (enumeration) --------------------------------------------------------
|
|
137
|
+
function readIndex(path) {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
140
|
+
return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function writeIndex(path, keys) {
|
|
147
|
+
writeSecureFile(path, JSON.stringify([...new Set(keys)]));
|
|
148
|
+
}
|
|
149
|
+
/** The keychain-backed `SecretBackend`. Delegates secret storage to the injected `runner` and
|
|
150
|
+
* maintains a keys-only index at `indexPath` for enumeration (`listKeys`/`clearAll`). */
|
|
151
|
+
export class KeychainBackend {
|
|
152
|
+
name = "keychain";
|
|
153
|
+
runner;
|
|
154
|
+
indexPath;
|
|
155
|
+
constructor(runner, indexPath) {
|
|
156
|
+
this.runner = runner;
|
|
157
|
+
this.indexPath = indexPath;
|
|
158
|
+
}
|
|
159
|
+
/** Side-effect-free availability probe: a `get` of a sentinel key, cancellable via `signal`.
|
|
160
|
+
* Only a clean response (`ok`/`not_found`) means the keychain is usable — `unavailable` (tool
|
|
161
|
+
* missing / no Secret Service) AND `error` (a locked keychain returning "user interaction is
|
|
162
|
+
* not allowed", etc.) both fall back to the `0600` file, which is the documented §5.2 path. */
|
|
163
|
+
async available(signal) {
|
|
164
|
+
const r = await this.runner({ action: "get", key: "__oh_hai_probe__" }, signal);
|
|
165
|
+
return r.status === "ok" || r.status === "not_found";
|
|
166
|
+
}
|
|
167
|
+
async get(key) {
|
|
168
|
+
const r = await this.runner({ action: "get", key });
|
|
169
|
+
if (r.status === "ok")
|
|
170
|
+
return r.value;
|
|
171
|
+
if (r.status === "error")
|
|
172
|
+
throw new Error(r.message ?? "keychain read failed");
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
async set(key, token) {
|
|
176
|
+
const r = await this.runner({ action: "set", key, token });
|
|
177
|
+
// A mutating op that comes back `unavailable` (the keychain vanished BETWEEN the probe and
|
|
178
|
+
// this write) stored nothing — treat it as a failure and DON'T record a phantom index entry,
|
|
179
|
+
// rather than silently reporting success (§5.2: an unavailable keychain must fall back/fail).
|
|
180
|
+
if (r.status === "error" || r.status === "unavailable") {
|
|
181
|
+
throw new Error(r.message ?? "keychain became unavailable; token not stored");
|
|
182
|
+
}
|
|
183
|
+
const keys = readIndex(this.indexPath);
|
|
184
|
+
if (!keys.includes(key))
|
|
185
|
+
writeIndex(this.indexPath, [...keys, key]);
|
|
186
|
+
}
|
|
187
|
+
async delete(key) {
|
|
188
|
+
const r = await this.runner({ action: "delete", key });
|
|
189
|
+
// Throw BEFORE touching the index (on `error` OR `unavailable`): pruning the key would strip
|
|
190
|
+
// it from the enumeration index while the secret is still in the keychain — orphaning a
|
|
191
|
+
// bearer that `logout`/`logout --all` could then never find or scrub.
|
|
192
|
+
if (r.status === "error" || r.status === "unavailable") {
|
|
193
|
+
throw new Error(r.message ?? "keychain became unavailable; token not removed");
|
|
194
|
+
}
|
|
195
|
+
const keys = readIndex(this.indexPath);
|
|
196
|
+
if (keys.includes(key))
|
|
197
|
+
writeIndex(this.indexPath, keys.filter((k) => k !== key));
|
|
198
|
+
return r.status === "ok";
|
|
199
|
+
}
|
|
200
|
+
listKeys() {
|
|
201
|
+
return Promise.resolve(readIndex(this.indexPath));
|
|
202
|
+
}
|
|
203
|
+
async clearAll() {
|
|
204
|
+
const keys = readIndex(this.indexPath);
|
|
205
|
+
// Delete each secret, but never drop a key from the index unless its keychain delete
|
|
206
|
+
// actually succeeded — a silently-ignored error would wipe the index while leaving the
|
|
207
|
+
// token in the keychain, orphaning it. Failed keys stay enumerable for a retry.
|
|
208
|
+
const failed = [];
|
|
209
|
+
let cleared = 0;
|
|
210
|
+
for (const key of keys) {
|
|
211
|
+
const r = await this.runner({ action: "delete", key });
|
|
212
|
+
if (r.status === "error") {
|
|
213
|
+
failed.push(key);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
cleared += 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (failed.length > 0) {
|
|
220
|
+
writeIndex(this.indexPath, failed);
|
|
221
|
+
throw new Error(`keychain delete failed for ${failed.length} of ${keys.length} identities`);
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
rmSync(this.indexPath, { force: true });
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
writeIndex(this.indexPath, []);
|
|
228
|
+
}
|
|
229
|
+
return cleared;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=keychain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../src/auth/keychain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ResolvedConfig, TokenSource } from "../config.js";
|
|
2
|
+
import type { CredentialStore } from "./token-store.js";
|
|
3
|
+
export interface ResolvedToken {
|
|
4
|
+
token: string | undefined;
|
|
5
|
+
source: TokenSource;
|
|
6
|
+
}
|
|
7
|
+
/** The effective identity: the resolved bearer plus the agent id it belongs to. `account` may be
|
|
8
|
+
* DISCOVERED (below) rather than configured, so it rides along with the token instead of the caller
|
|
9
|
+
* reading it off `config.account`. */
|
|
10
|
+
export interface ResolvedIdentity {
|
|
11
|
+
account: string | undefined;
|
|
12
|
+
token: string | undefined;
|
|
13
|
+
source: TokenSource;
|
|
14
|
+
}
|
|
15
|
+
/** Derive the Hub origin (scheme+host+port) from a resolved base URL, for Hub-scoped keying
|
|
16
|
+
* (§5.1). Falls back to the raw string if the URL can't be parsed (resolveConfig guarantees a
|
|
17
|
+
* valid default, so this is belt-and-suspenders). */
|
|
18
|
+
export declare function originOf(baseUrl: string): string;
|
|
19
|
+
/** Resolve the effective bearer token and where it came from, per §6. `store` may be omitted
|
|
20
|
+
* when the caller already knows resolution won't touch it (an env token, or no account). */
|
|
21
|
+
export declare function resolveToken(config: ResolvedConfig, store?: CredentialStore): Promise<ResolvedToken>;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the effective identity (§6): the account plus its bearer. Extends `resolveToken` with a
|
|
24
|
+
* zero-config fallback for the device-code login path, where the bearer is stored under a
|
|
25
|
+
* Hub-chosen agent id but no `--account`/`MA2H_AGENT_ID`/config account is set. When no account is
|
|
26
|
+
* configured and the token isn't the CI env credential, fall back to the SOLE stored identity for
|
|
27
|
+
* the current Hub origin — so a fresh `oh-hai login` then `oh-hai whoami` resolves without a manual
|
|
28
|
+
* `--account`. Ambiguity is never guessed: with zero or more-than-one stored identity for the
|
|
29
|
+
* origin, the account stays undefined and the user selects with `--account` (§6). The env path is
|
|
30
|
+
* never discovered — the env credential is authoritative and its agent id may differ from a stored
|
|
31
|
+
* login. Discovery keys on the origin (derived from the flag/env/user-config `base_url`, never a
|
|
32
|
+
* repo-controlled value), so it preserves §5.1 isolation and can't leak a bearer to a chosen Hub.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveIdentity(config: ResolvedConfig, store?: CredentialStore): Promise<ResolvedIdentity>;
|