@kynver-app/runtime 0.2.138 → 0.2.140
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 +135 -135
- package/dist/chat/chat-access-change-record.js +1 -1
- package/dist/chat/chat-turn-envelope.js +1 -1
- package/dist/chat/command-auth-proof.js +1 -1
- package/dist/cli.js +208 -206
- package/dist/daemon-tick-thread.js +6 -4
- package/dist/desktop-entry.js +159 -157
- package/dist/dispatch-claim-authorization.js +1 -1
- package/dist/index.js +232 -230
- package/dist/instruction-bundle/contract.js +2 -2
- package/dist/local-run-store/index.js +10 -8
- package/dist/server/cleanup.js +3 -1
- package/dist/server/default-repo.js +3 -1
- package/dist/server/dispatch-routing.js +3 -1
- package/dist/server/model-routing.js +3 -1
- package/dist/server/monitor.js +3 -1
- package/dist/server/orchestration.js +5 -3
- package/dist/server/worker-policy.js +6 -4
- package/package.json +225 -224
- package/scripts/postinstall-banner.mjs +28 -0
package/README.md
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
# @kynver-app/runtime
|
|
2
|
-
|
|
3
|
-
Standalone Kynver AgentOS execution runtime and CLI (`kynver`) — run Kynver's agent harness on **your own machine**, on your own provider subscriptions. Provider credentials stay local to the box; the daemon talks to Kynver over scoped, revocable `krc1.*` runner tokens.
|
|
4
|
-
|
|
5
|
-
## Requirements
|
|
6
|
-
|
|
7
|
-
- **Node.js 24.11.0+** (npm 11). This is a hard engine floor: below it the CLI **fails closed at startup** — every work-executing command refuses to run, and only `--version` and the `doctor` diagnostic commands stay reachable for diagnosis. Check any host with:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
kynver doctor node-runtime # report-only JSON classification + remediation
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
- A worker-provider CLI on PATH with an active login/subscription (see [Worker providers](#worker-providers)).
|
|
14
|
-
- **Agentic OS access** on your Kynver account. Access is **admin-approved**: if `kynver bootstrap` reports that your account has no access, request it at <https://www.kynver.com/agent-os/early-access> — once approved, your account works here with no further changes (just re-run `kynver bootstrap`).
|
|
15
|
-
|
|
16
|
-
## Quick start
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install -g @kynver-app/runtime
|
|
20
|
-
kynver
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
That's it. The first time, `kynver` links this computer to your Kynver account: it opens the approval page in your browser (the link and code are printed too), you click approve, and it sets up this computer and brings Kynver online. After that, `kynver` just starts it. If Kynver is already running on this computer it says "Kynver is already running ✓" and does nothing else. `kynver stop` stops it.
|
|
24
|
-
|
|
25
|
-
While it runs, the window stays calm: a short status block ("Kynver is online ✓ · linked to … · chat: Claude Code"), then one line per chat message it answers. Problems are printed in plain words with the command that fixes them. The full log is always in `~/.kynver/logs/kynver.log`; `kynver --verbose` (or `KYNVER_VERBOSE=1`) shows it in the window as well.
|
|
26
|
-
|
|
27
|
-
**Website chat on this computer** turns on by itself when Claude Code (or Codex) is installed, and `kynver` says so once: "Website chat will use Claude Code on this computer. Turn off: kynver setup --no-chat-oauth". The choice is saved, so `kynver` and `kynver start` follow it with no flag.
|
|
28
|
-
|
|
29
|
-
Other Kynver sites (staging, local dev): `kynver --api-base-url https://your-kynver-site`. For a persistent background agent on Linux: `kynver start --install-systemd`. The step-by-step commands (`kynver bootstrap`, `kynver setup`, `kynver start`, `kynver daemon --run …`) still work exactly as before.
|
|
30
|
-
|
|
31
|
-
### Windows: Kynver Desktop
|
|
32
|
-
|
|
33
|
-
On Windows x64 there is also **Kynver Desktop**, which bundles this runtime and its own Node.js, so there is no npm
|
|
34
|
-
install and no terminal. It is offered in **Account → Settings** (next to these commands) only once a release is
|
|
35
|
-
published, and a scenario is declared supported only after it has been tested end to end; until then the command line
|
|
36
|
-
above is the way in. Only one runtime may work for a workspace on a computer: quit Desktop before running
|
|
37
|
-
`kynver`, and stop the CLI (`kynver stop`) before starting Desktop.
|
|
38
|
-
|
|
39
|
-
### What `kynver bootstrap` does
|
|
40
|
-
|
|
41
|
-
1. **Device login** — prints a URL and a short code; approve in the browser. No API-key pasting; the minted key is saved to `~/.kynver/credentials`.
|
|
42
|
-
2. **Workspace resolution** — resolves your account's primary AgentOS workspace. If the account is not approved for Agentic OS yet, bootstrap says so and links the request page (see Requirements above).
|
|
43
|
-
3. **Machine setup** — persists `~/.kynver/config.json` (API base URL, workspace id, repo discovery, worker caps, box identity) and mints a scoped `krc1.*` runner credential.
|
|
44
|
-
|
|
45
|
-
Bootstrap does **not** start any work — bringing workers online is `kynver` (or `kynver start`). Bare `kynver` runs bootstrap for you on a computer that isn't linked yet.
|
|
46
|
-
|
|
47
|
-
### Troubleshooting → `kynver doctor runner-install`
|
|
48
|
-
|
|
49
|
-
If `kynver` (or `bootstrap` / `start`) fails, or the agent seems dead:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
kynver doctor runner-install
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
It walks the whole first-run lifecycle — install, device login, workspace binding, repo selection, runner credential, daemon liveness — plus the first-run blockers: worker-provider CLI on PATH, `boxKind` identity, repo base ref (`origin/main` / remote default branch), and live API reachability/auth (a 401 means a revoked key → `kynver login`; a 403 means access is gated → request it at the early-access page). Every failing check carries the exact command that fixes it.
|
|
56
|
-
|
|
57
|
-
## Report a bug
|
|
58
|
-
|
|
59
|
-
\`kynver report-bug\` prepares a draft locally — **it does not send anything**. After you create a GitHub issue, the report appears at [github.com/Totalsolutionsync/kynver-feedback/issues](https://github.com/Totalsolutionsync/kynver-feedback/issues).
|
|
60
|
-
|
|
61
|
-
\`\`\`bash
|
|
62
|
-
kynver report-bug --json # agents: read submission.autoSubmit (false) and guide.agentSteps
|
|
63
|
-
\`\`\`
|
|
64
|
-
|
|
65
|
-
Install/runtime → [install template](https://github.com/Totalsolutionsync/kynver-feedback/issues/new?template=install_bug.yml). Other product bugs → [product template](https://github.com/Totalsolutionsync/kynver-feedback/issues/new?template=product_bug.yml). Guide: [kynver.com/help/bug-reports](https://www.kynver.com/help/bug-reports).
|
|
66
|
-
|
|
67
|
-
### Callback auth (scoped runner tokens)
|
|
68
|
-
|
|
69
|
-
User-hosted runners should **not** use deployment-wide `OPENCLAW_CRON_SECRET` / `KYNVER_RUNTIME_SECRET`. `kynver bootstrap` mints a workspace-bound token automatically; to re-mint one:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
kynver runner credential --agent-os-id <id>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
The token (`krc1.*`) is stored in `~/.kynver/credentials` as `runnerToken` and sent as `X-Kynver-Runner-Token` on dispatch/sweep/completion/progress callbacks. It is bound to one `agentOsId` and a capability set (`tasks`, `harness`, `plans.progress`, `runtime.read`, `operator`). The server persists only a token hash plus audit metadata, so credentials can be listed, revoked, and rotated without exposing the clear token again.
|
|
76
|
-
|
|
77
|
-
Global env secrets are accepted on by-id callbacks **only** when the deployment lists the workspace in `KYNVER_HARNESS_GLOBAL_SECRET_AGENT_OS_IDS` (first-party dogfood). Self-serve runners must use scoped `krc1.*` tokens. QStash-signed scheduler callbacks and internal cron fire routes are unchanged.
|
|
78
|
-
|
|
79
|
-
## CLI verbs
|
|
80
|
-
|
|
81
|
-
```text
|
|
82
|
-
(no verb) — start Kynver (links this computer first if needed)
|
|
83
|
-
stop | bootstrap | start | status | setup | login | daemon
|
|
84
|
-
run create | list | status | dispatch | sweep
|
|
85
|
-
worker start | status | tail | stop | complete
|
|
86
|
-
runner credential [mint|status|list|revoke|rotate]
|
|
87
|
-
config ensure-default-repo
|
|
88
|
-
doctor runner-install | node-runtime | local-store | context-envelope | runtime-takeover
|
|
89
|
-
report-bug [--json] [--message TEXT] [--skip-doctor]
|
|
90
|
-
browser status | install [--deps] [--force]
|
|
91
|
-
agents status [--json] | setup hermes|openclaw|claude-code|grok-bot [--dry-run]
|
|
92
|
-
tab post --conversation <id> [--file <path>]... [--key <k>] <text|-|@file>
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Run `kynver help` for the full flag-level usage.
|
|
96
|
-
|
|
97
|
-
## Chat browser
|
|
98
|
-
|
|
99
|
-
When website chat uses the browser on your computer, Kynver picks one automatically: a path you configured, then an installed Chrome / Chromium / Edge, then its own copy. If there is none, Kynver downloads Chrome for Testing (Google's official build, about 150 MB, once) into `~/.kynver/browsers` — no sudo. `kynver browser status` shows what will be used; `kynver browser install` downloads it now. On a minimal Linux or WSL install Chrome may also need system libraries: `kynver browser status` prints the exact `apt-get` / `dnf` command, and `kynver browser install --deps` runs it when sudo needs no password. On WSL the browser window appears on your Windows desktop through WSLg; without a display it runs headless. Turn the automatic download off with `"browserAutoProvision": false` in `~/.kynver/config.json`.
|
|
100
|
-
|
|
101
|
-
## Agent tabs (Hermes, OpenClaw, Claude Code, Grok Bot)
|
|
102
|
-
|
|
103
|
-
Kynver chat can open a tab for each agent you run on this computer: your Hermes Agent, your OpenClaw agent, your own signed-in Claude Code, or Grok Bot. The daemon talks to Hermes, OpenClaw and Claude Code on localhost and relays messages over its existing Kynver connection — nothing is exposed to the internet and their keys never leave this computer. `kynver agents status` shows what was found. `kynver agents setup hermes` switches on Hermes's local API server in `~/.hermes/.env` (with a new local-only key); restart the Hermes gateway afterwards. `kynver agents setup openclaw` switches on the OpenClaw gateway's chat API and restarts it. Claude Code needs only `claude` signed in and a folder chosen for chat computer access. Grok Bot is detected when the desktop app is installed (`grok-bot` on PATH, the Linux package, or the app's config directory). It has no published connection that accepts a message and returns the reply, so `kynver agents setup grok-bot` explains that and exits with an error. The Kynver tab keeps the name you set and does not deliver chat. Steps are logged to `~/.kynver/logs/agent-tabs.jsonl` (no message text); session ids per tab live in `~/.kynver/agent-tab-sessions.json`. `KYNVER_OPENCLAW_AGENT` picks a non-default OpenClaw agent; `KYNVER_AGENT_TAB_CLAUDE_MODEL` pins a Claude Code model (default: your own Claude Code default).
|
|
104
|
-
|
|
105
|
-
**Updates about work go to the chat that started it.** Each tab message tells the agent which Kynver conversation it is in: Hermes and OpenClaw get a line in the turn's system note (and an `X-Kynver-Conversation` header); Claude Code gets `KYNVER_CONVERSATION` (and `KYNVER_AGENT_TAB_ENGINE`) in its environment. When work started there finishes, fails, needs you or opens a PR, the agent posts back with `kynver tab post --conversation <id> "PR is green"` (`--file <path>` attaches files, big ones are uploaded first; `-` reads the text from stdin, `@notes.md` from a file; the same `--key` never posts twice). It defaults to `$KYNVER_CONVERSATION` and refuses with neither — it never falls back to whatever conversation is open. Kynver tasks created with `originConversationId` post their own finished / failed / needs-you / PR updates into that conversation automatically.
|
|
106
|
-
|
|
107
|
-
## Worker providers
|
|
108
|
-
|
|
109
|
-
Set once in `kynver setup --provider claude|cursor|codex|gemini|gemini-api|openai-compatible` (stored in `~/.kynver/config.json` as `workerProvider`). `--provider chatgpt` / `gpt` are aliases for `codex`.
|
|
110
|
-
|
|
111
|
-
| Provider | CLI / endpoint | Auth |
|
|
112
|
-
| --- | --- | --- |
|
|
113
|
-
| `cursor` (default) | `agent` on PATH ([Cursor Agent CLI](https://cursor.com/docs/cli/headless)) | `agent login` (OAuth) **or** `CURSOR_API_KEY` for headless runners. **Website chat uses this same CLI** when `workerProvider`/`chatProvider` is `cursor` — `agent` on PATH + `agent login` (or `CURSOR_API_KEY`). Missing/logged-out fails closed (no hosted-model fallback). |
|
|
114
|
-
| `codex` (ChatGPT on your machine) | `codex` on PATH ([Codex CLI](https://developers.openai.com/codex/)) **or** `hermes` with `openai-codex` | `codex login` / `CODEX_API_KEY`, or `hermes auth status openai-codex`. **Website chat uses this same CLI** when `workerProvider`/`chatProvider` is `chatgpt` / `codex` / `gpt` — missing/logged-out fails closed (no hosted-model or Claude fallback). You do not need `OPENAI_API_KEY` for this path. Workers also spawn `codex exec`. When the standalone `codex` CLI is absent, harness workers can fall back to `hermes` openai-codex. |
|
|
115
|
-
| `claude` (operator override) | `claude` on PATH | `claude login` (OAuth). Use `--provider claude` on dispatch/worker start, or set `executorRef: provider:claude` / `[worker-provider: claude]` on the board task. |
|
|
116
|
-
| `gemini` | Google Antigravity (`agy` on PATH, [install](https://antigravity.google/docs/cli/install)) | `kynver setup --provider gemini`, install Google Antigravity, then run `agy` once to sign in. **Website chat uses this same CLI** — no Gemini API key. A Gemini API key works only when `~/.gemini/antigravity-cli/settings.json` sets `"modelProvider": "gemini"` and `GEMINI_API_KEY` is set (still the CLI, not HTTP). Workers run `agy --prompt --output-format stream-json`. Set `KYNVER_GEMINI_BIN=gemini` only for a legacy enterprise `gemini` binary; that binary's website chat stays the HTTP key path. Missing binary fails closed. |
|
|
117
|
-
| `gemini-api` | Google Gemini over HTTP (no local CLI). Default `https://generativelanguage.googleapis.com/v1beta/openai` | `KYNVER_GEMINI_API_KEY` or `GEMINI_API_KEY` — read in the daemon parent and handed to the runner over stdin, never argv/child env. Override the endpoint with `KYNVER_GEMINI_BASE_URL`. |
|
|
118
|
-
| `openai-compatible` (aliases `openai`, `openai-api`) | Any OpenAI-compatible `/v1` chat-completions URL | `KYNVER_WORKER_BASE_URL` (required) + optional `KYNVER_WORKER_API_KEY` over stdin. This is the generic HTTP path — not the ChatGPT product option. |
|
|
119
|
-
|
|
120
|
-
The `gemini-api` HTTP route and a legacy `gemini` binary use `KYNVER_GEMINI_DEFAULT_MODEL` (default `gemini-2.5-pro`). Google Antigravity omits that id at spawn so `agy` keeps the signed-in account's model; pass an Antigravity slug such as `gemini-3.1-pro-high` when you want a specific one. Choose the route explicitly: `--provider gemini` vs `--provider gemini-api`, or `executorRef: provider:gemini[:model]` / `provider:gemini-api[:model]`. ChatGPT on a box: `--provider chatgpt` or `--provider codex` (same Codex CLI), or `executorRef: provider:chatgpt[:model]`.
|
|
121
|
-
|
|
122
|
-
Override per invocation: `kynver worker start ... --provider gemini` (requires operator intent; default dispatches stay on Cursor)
|
|
123
|
-
|
|
124
|
-
Install Cursor CLI (Windows PowerShell): `irm 'https://cursor.com/install?win32=true' | iex`
|
|
125
|
-
|
|
126
|
-
Default Cursor model: `composer-2.5`. Override with `--model` on dispatch/worker start.
|
|
127
|
-
|
|
128
|
-
Website chat (delegated turns claimed by `kynver daemon`) follows `chatProvider` if set, otherwise `workerProvider`. Cursor boxes spawn `agent --print --mode agent` by default in `~/.kynver/chat-workspace` (created empty — no monorepo clone, no `defaultRepo` assumption). ChatGPT boxes spawn `codex exec --json` in that same workspace (`codex login` or `CODEX_API_KEY`). Google Antigravity boxes spawn `agy` in that same workspace (run `agy` once; a bare API key does not select HTTP). `gemini-api` and a legacy `gemini` binary still call Google over HTTP (`GEMINI_API_KEY` / `KYNVER_GEMINI_API_KEY`). A missing Codex CLI / login fails closed (`codex_cli_unavailable` / `codex_not_logged_in`). A missing or signed-out `agy` fails closed (`antigravity_cli_unavailable` / `antigravity_not_logged_in`) — no silent hosted-model fallback. Ask mode is only used when the website toggle requests it. The hosted personal-agent loop handles Agent-mode / factory turns so MEMBER users can call factory tools. Claude/Anthropic boxes keep the local Anthropic Messages path (`ANTHROPIC_API_KEY` or `kynver start --chat-oauth`). Command turns are unchanged (allowlist + auth proof).
|
|
129
|
-
|
|
130
|
-
## Contributing
|
|
131
|
-
|
|
132
|
-
Development against a Kynver monorepo checkout is documented for
|
|
133
|
-
contributors inside the Kynver repository.
|
|
134
|
-
|
|
135
|
-
Bug reports: see [Report a bug](#report-a-bug) above.
|
|
1
|
+
# @kynver-app/runtime
|
|
2
|
+
|
|
3
|
+
Standalone Kynver AgentOS execution runtime and CLI (`kynver`) — run Kynver's agent harness on **your own machine**, on your own provider subscriptions. Provider credentials stay local to the box; the daemon talks to Kynver over scoped, revocable `krc1.*` runner tokens.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- **Node.js 24.11.0+** (npm 11). This is a hard engine floor: below it the CLI **fails closed at startup** — every work-executing command refuses to run, and only `--version` and the `doctor` diagnostic commands stay reachable for diagnosis. Check any host with:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
kynver doctor node-runtime # report-only JSON classification + remediation
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- A worker-provider CLI on PATH with an active login/subscription (see [Worker providers](#worker-providers)).
|
|
14
|
+
- **Agentic OS access** on your Kynver account. Access is **admin-approved**: if `kynver bootstrap` reports that your account has no access, request it at <https://www.kynver.com/agent-os/early-access> — once approved, your account works here with no further changes (just re-run `kynver bootstrap`).
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @kynver-app/runtime
|
|
20
|
+
kynver
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That's it. The first time, `kynver` links this computer to your Kynver account: it opens the approval page in your browser (the link and code are printed too), you click approve, and it sets up this computer and brings Kynver online. After that, `kynver` just starts it. If Kynver is already running on this computer it says "Kynver is already running ✓" and does nothing else. `kynver stop` stops it.
|
|
24
|
+
|
|
25
|
+
While it runs, the window stays calm: a short status block ("Kynver is online ✓ · linked to … · chat: Claude Code"), then one line per chat message it answers. Problems are printed in plain words with the command that fixes them. The full log is always in `~/.kynver/logs/kynver.log`; `kynver --verbose` (or `KYNVER_VERBOSE=1`) shows it in the window as well.
|
|
26
|
+
|
|
27
|
+
**Website chat on this computer** turns on by itself when Claude Code (or Codex) is installed, and `kynver` says so once: "Website chat will use Claude Code on this computer. Turn off: kynver setup --no-chat-oauth". The choice is saved, so `kynver` and `kynver start` follow it with no flag.
|
|
28
|
+
|
|
29
|
+
Other Kynver sites (staging, local dev): `kynver --api-base-url https://your-kynver-site`. For a persistent background agent on Linux: `kynver start --install-systemd`. The step-by-step commands (`kynver bootstrap`, `kynver setup`, `kynver start`, `kynver daemon --run …`) still work exactly as before.
|
|
30
|
+
|
|
31
|
+
### Windows: Kynver Desktop
|
|
32
|
+
|
|
33
|
+
On Windows x64 there is also **Kynver Desktop**, which bundles this runtime and its own Node.js, so there is no npm
|
|
34
|
+
install and no terminal. It is offered in **Account → Settings** (next to these commands) only once a release is
|
|
35
|
+
published, and a scenario is declared supported only after it has been tested end to end; until then the command line
|
|
36
|
+
above is the way in. Only one runtime may work for a workspace on a computer: quit Desktop before running
|
|
37
|
+
`kynver`, and stop the CLI (`kynver stop`) before starting Desktop.
|
|
38
|
+
|
|
39
|
+
### What `kynver bootstrap` does
|
|
40
|
+
|
|
41
|
+
1. **Device login** — prints a URL and a short code; approve in the browser. No API-key pasting; the minted key is saved to `~/.kynver/credentials`.
|
|
42
|
+
2. **Workspace resolution** — resolves your account's primary AgentOS workspace. If the account is not approved for Agentic OS yet, bootstrap says so and links the request page (see Requirements above).
|
|
43
|
+
3. **Machine setup** — persists `~/.kynver/config.json` (API base URL, workspace id, repo discovery, worker caps, box identity) and mints a scoped `krc1.*` runner credential.
|
|
44
|
+
|
|
45
|
+
Bootstrap does **not** start any work — bringing workers online is `kynver` (or `kynver start`). Bare `kynver` runs bootstrap for you on a computer that isn't linked yet.
|
|
46
|
+
|
|
47
|
+
### Troubleshooting → `kynver doctor runner-install`
|
|
48
|
+
|
|
49
|
+
If `kynver` (or `bootstrap` / `start`) fails, or the agent seems dead:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
kynver doctor runner-install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It walks the whole first-run lifecycle — install, device login, workspace binding, repo selection, runner credential, daemon liveness — plus the first-run blockers: worker-provider CLI on PATH, `boxKind` identity, repo base ref (`origin/main` / remote default branch), and live API reachability/auth (a 401 means a revoked key → `kynver login`; a 403 means access is gated → request it at the early-access page). Every failing check carries the exact command that fixes it.
|
|
56
|
+
|
|
57
|
+
## Report a bug
|
|
58
|
+
|
|
59
|
+
\`kynver report-bug\` prepares a draft locally — **it does not send anything**. After you create a GitHub issue, the report appears at [github.com/Totalsolutionsync/kynver-feedback/issues](https://github.com/Totalsolutionsync/kynver-feedback/issues).
|
|
60
|
+
|
|
61
|
+
\`\`\`bash
|
|
62
|
+
kynver report-bug --json # agents: read submission.autoSubmit (false) and guide.agentSteps
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
Install/runtime → [install template](https://github.com/Totalsolutionsync/kynver-feedback/issues/new?template=install_bug.yml). Other product bugs → [product template](https://github.com/Totalsolutionsync/kynver-feedback/issues/new?template=product_bug.yml). Guide: [kynver.com/help/bug-reports](https://www.kynver.com/help/bug-reports).
|
|
66
|
+
|
|
67
|
+
### Callback auth (scoped runner tokens)
|
|
68
|
+
|
|
69
|
+
User-hosted runners should **not** use deployment-wide `OPENCLAW_CRON_SECRET` / `KYNVER_RUNTIME_SECRET`. `kynver bootstrap` mints a workspace-bound token automatically; to re-mint one:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
kynver runner credential --agent-os-id <id>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The token (`krc1.*`) is stored in `~/.kynver/credentials` as `runnerToken` and sent as `X-Kynver-Runner-Token` on dispatch/sweep/completion/progress callbacks. It is bound to one `agentOsId` and a capability set (`tasks`, `harness`, `plans.progress`, `runtime.read`, `operator`). The server persists only a token hash plus audit metadata, so credentials can be listed, revoked, and rotated without exposing the clear token again.
|
|
76
|
+
|
|
77
|
+
Global env secrets are accepted on by-id callbacks **only** when the deployment lists the workspace in `KYNVER_HARNESS_GLOBAL_SECRET_AGENT_OS_IDS` (first-party dogfood). Self-serve runners must use scoped `krc1.*` tokens. QStash-signed scheduler callbacks and internal cron fire routes are unchanged.
|
|
78
|
+
|
|
79
|
+
## CLI verbs
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
(no verb) — start Kynver (links this computer first if needed)
|
|
83
|
+
stop | bootstrap | start | status | setup | login | daemon
|
|
84
|
+
run create | list | status | dispatch | sweep
|
|
85
|
+
worker start | status | tail | stop | complete
|
|
86
|
+
runner credential [mint|status|list|revoke|rotate]
|
|
87
|
+
config ensure-default-repo
|
|
88
|
+
doctor runner-install | node-runtime | local-store | context-envelope | runtime-takeover
|
|
89
|
+
report-bug [--json] [--message TEXT] [--skip-doctor]
|
|
90
|
+
browser status | install [--deps] [--force]
|
|
91
|
+
agents status [--json] | setup hermes|openclaw|claude-code|grok-bot [--dry-run]
|
|
92
|
+
tab post --conversation <id> [--file <path>]... [--key <k>] <text|-|@file>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Run `kynver help` for the full flag-level usage.
|
|
96
|
+
|
|
97
|
+
## Chat browser
|
|
98
|
+
|
|
99
|
+
When website chat uses the browser on your computer, Kynver picks one automatically: a path you configured, then an installed Chrome / Chromium / Edge, then its own copy. If there is none, Kynver downloads Chrome for Testing (Google's official build, about 150 MB, once) into `~/.kynver/browsers` — no sudo. `kynver browser status` shows what will be used; `kynver browser install` downloads it now. On a minimal Linux or WSL install Chrome may also need system libraries: `kynver browser status` prints the exact `apt-get` / `dnf` command, and `kynver browser install --deps` runs it when sudo needs no password. On WSL the browser window appears on your Windows desktop through WSLg; without a display it runs headless. Turn the automatic download off with `"browserAutoProvision": false` in `~/.kynver/config.json`.
|
|
100
|
+
|
|
101
|
+
## Agent tabs (Hermes, OpenClaw, Claude Code, Grok Bot)
|
|
102
|
+
|
|
103
|
+
Kynver chat can open a tab for each agent you run on this computer: your Hermes Agent, your OpenClaw agent, your own signed-in Claude Code, or Grok Bot. The daemon talks to Hermes, OpenClaw and Claude Code on localhost and relays messages over its existing Kynver connection — nothing is exposed to the internet and their keys never leave this computer. `kynver agents status` shows what was found. `kynver agents setup hermes` switches on Hermes's local API server in `~/.hermes/.env` (with a new local-only key); restart the Hermes gateway afterwards. `kynver agents setup openclaw` switches on the OpenClaw gateway's chat API and restarts it. Claude Code needs only `claude` signed in and a folder chosen for chat computer access. Grok Bot is detected when the desktop app is installed (`grok-bot` on PATH, the Linux package, or the app's config directory). It has no published connection that accepts a message and returns the reply, so `kynver agents setup grok-bot` explains that and exits with an error. The Kynver tab keeps the name you set and does not deliver chat. Steps are logged to `~/.kynver/logs/agent-tabs.jsonl` (no message text); session ids per tab live in `~/.kynver/agent-tab-sessions.json`. `KYNVER_OPENCLAW_AGENT` picks a non-default OpenClaw agent; `KYNVER_AGENT_TAB_CLAUDE_MODEL` pins a Claude Code model (default: your own Claude Code default).
|
|
104
|
+
|
|
105
|
+
**Updates about work go to the chat that started it.** Each tab message tells the agent which Kynver conversation it is in: Hermes and OpenClaw get a line in the turn's system note (and an `X-Kynver-Conversation` header); Claude Code gets `KYNVER_CONVERSATION` (and `KYNVER_AGENT_TAB_ENGINE`) in its environment. When work started there finishes, fails, needs you or opens a PR, the agent posts back with `kynver tab post --conversation <id> "PR is green"` (`--file <path>` attaches files, big ones are uploaded first; `-` reads the text from stdin, `@notes.md` from a file; the same `--key` never posts twice). It defaults to `$KYNVER_CONVERSATION` and refuses with neither — it never falls back to whatever conversation is open. Kynver tasks created with `originConversationId` post their own finished / failed / needs-you / PR updates into that conversation automatically.
|
|
106
|
+
|
|
107
|
+
## Worker providers
|
|
108
|
+
|
|
109
|
+
Set once in `kynver setup --provider claude|cursor|codex|gemini|gemini-api|openai-compatible` (stored in `~/.kynver/config.json` as `workerProvider`). `--provider chatgpt` / `gpt` are aliases for `codex`.
|
|
110
|
+
|
|
111
|
+
| Provider | CLI / endpoint | Auth |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| `cursor` (default) | `agent` on PATH ([Cursor Agent CLI](https://cursor.com/docs/cli/headless)) | `agent login` (OAuth) **or** `CURSOR_API_KEY` for headless runners. **Website chat uses this same CLI** when `workerProvider`/`chatProvider` is `cursor` — `agent` on PATH + `agent login` (or `CURSOR_API_KEY`). Missing/logged-out fails closed (no hosted-model fallback). |
|
|
114
|
+
| `codex` (ChatGPT on your machine) | `codex` on PATH ([Codex CLI](https://developers.openai.com/codex/)) **or** `hermes` with `openai-codex` | `codex login` / `CODEX_API_KEY`, or `hermes auth status openai-codex`. **Website chat uses this same CLI** when `workerProvider`/`chatProvider` is `chatgpt` / `codex` / `gpt` — missing/logged-out fails closed (no hosted-model or Claude fallback). You do not need `OPENAI_API_KEY` for this path. Workers also spawn `codex exec`. When the standalone `codex` CLI is absent, harness workers can fall back to `hermes` openai-codex. |
|
|
115
|
+
| `claude` (operator override) | `claude` on PATH | `claude login` (OAuth). Use `--provider claude` on dispatch/worker start, or set `executorRef: provider:claude` / `[worker-provider: claude]` on the board task. |
|
|
116
|
+
| `gemini` | Google Antigravity (`agy` on PATH, [install](https://antigravity.google/docs/cli/install)) | `kynver setup --provider gemini`, install Google Antigravity, then run `agy` once to sign in. **Website chat uses this same CLI** — no Gemini API key. A Gemini API key works only when `~/.gemini/antigravity-cli/settings.json` sets `"modelProvider": "gemini"` and `GEMINI_API_KEY` is set (still the CLI, not HTTP). Workers run `agy --prompt --output-format stream-json`. Set `KYNVER_GEMINI_BIN=gemini` only for a legacy enterprise `gemini` binary; that binary's website chat stays the HTTP key path. Missing binary fails closed. |
|
|
117
|
+
| `gemini-api` | Google Gemini over HTTP (no local CLI). Default `https://generativelanguage.googleapis.com/v1beta/openai` | `KYNVER_GEMINI_API_KEY` or `GEMINI_API_KEY` — read in the daemon parent and handed to the runner over stdin, never argv/child env. Override the endpoint with `KYNVER_GEMINI_BASE_URL`. |
|
|
118
|
+
| `openai-compatible` (aliases `openai`, `openai-api`) | Any OpenAI-compatible `/v1` chat-completions URL | `KYNVER_WORKER_BASE_URL` (required) + optional `KYNVER_WORKER_API_KEY` over stdin. This is the generic HTTP path — not the ChatGPT product option. |
|
|
119
|
+
|
|
120
|
+
The `gemini-api` HTTP route and a legacy `gemini` binary use `KYNVER_GEMINI_DEFAULT_MODEL` (default `gemini-2.5-pro`). Google Antigravity omits that id at spawn so `agy` keeps the signed-in account's model; pass an Antigravity slug such as `gemini-3.1-pro-high` when you want a specific one. Choose the route explicitly: `--provider gemini` vs `--provider gemini-api`, or `executorRef: provider:gemini[:model]` / `provider:gemini-api[:model]`. ChatGPT on a box: `--provider chatgpt` or `--provider codex` (same Codex CLI), or `executorRef: provider:chatgpt[:model]`.
|
|
121
|
+
|
|
122
|
+
Override per invocation: `kynver worker start ... --provider gemini` (requires operator intent; default dispatches stay on Cursor)
|
|
123
|
+
|
|
124
|
+
Install Cursor CLI (Windows PowerShell): `irm 'https://cursor.com/install?win32=true' | iex`
|
|
125
|
+
|
|
126
|
+
Default Cursor model: `composer-2.5`. Override with `--model` on dispatch/worker start.
|
|
127
|
+
|
|
128
|
+
Website chat (delegated turns claimed by `kynver daemon`) follows `chatProvider` if set, otherwise `workerProvider`. Cursor boxes spawn `agent --print --mode agent` by default in `~/.kynver/chat-workspace` (created empty — no monorepo clone, no `defaultRepo` assumption). ChatGPT boxes spawn `codex exec --json` in that same workspace (`codex login` or `CODEX_API_KEY`). Google Antigravity boxes spawn `agy` in that same workspace (run `agy` once; a bare API key does not select HTTP). `gemini-api` and a legacy `gemini` binary still call Google over HTTP (`GEMINI_API_KEY` / `KYNVER_GEMINI_API_KEY`). A missing Codex CLI / login fails closed (`codex_cli_unavailable` / `codex_not_logged_in`). A missing or signed-out `agy` fails closed (`antigravity_cli_unavailable` / `antigravity_not_logged_in`) — no silent hosted-model fallback. Ask mode is only used when the website toggle requests it. The hosted personal-agent loop handles Agent-mode / factory turns so MEMBER users can call factory tools. Claude/Anthropic boxes keep the local Anthropic Messages path (`ANTHROPIC_API_KEY` or `kynver start --chat-oauth`). Command turns are unchanged (allowlist + auth proof).
|
|
129
|
+
|
|
130
|
+
## Contributing
|
|
131
|
+
|
|
132
|
+
Development against a Kynver monorepo checkout is documented for
|
|
133
|
+
contributors inside the Kynver repository.
|
|
134
|
+
|
|
135
|
+
Bug reports: see [Report a bug](#report-a-bug) above.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{verify as k,createPublicKey as x}from"node:crypto";function c(e){return JSON.stringify(u(e))}function u(e){if(Array.isArray(e))return e.map(u);if(e&&typeof e=="object"){let n={};for(let r of Object.keys(e).sort())n[r]=u(e[r]);return n}return e}function d(e){if(!e)return null;let n=e.trim();if(!n)return null;if(/^[0-9a-fA-F]{64}$/.test(n))return new Uint8Array(n.match(/.{2}/g).map(r=>parseInt(r,16)));try{let r=Buffer.from(n,"base64");if(r.length===32)return new Uint8Array(r)}catch{}return null}function p(e){if(!e)return[];let n=[];for(let r of e.split(/[,\s]+/)){let t=d(r);t&&n.push(t)}return n}function y(e=process.env){if(e.NODE_ENV==="production")return!1;let n=e.KYNVER_ALLOW_DEV_SIGNING_KEYS?.trim().toLowerCase();return n==="1"||n==="true"||n==="yes"||n==="on"?!0:e.NODE_ENV==="test"||e.VITEST==="true"}import{createHash as A,createPublicKey as B,verify as O}from"node:crypto";var N=Buffer.from("302a300506032b6570032100","hex"),h="ca-6297fb8e1773",C={[h]:"16ae5cb2a61f8c94cf1b86e8258f9c2f2ff6aba5e6136250a422209b6e866881","ca-456fd128f3b3":"f29cbd559410c2578d7f9732ccd86570073355ccdb36af0d1b7dff15908466e7"};function I(e){return`ca-${A("sha256").update(Buffer.from(e)).digest("hex").slice(0,12)}`}function m(e,n=process.env){let r=p(n.KYNVER_COMMAND_AUTH_PUBLIC_KEY);if(r.length>0)return r.find(a=>I(a)===e)??null;let t=C[e];return!t||e===h&&!y(n)?null:d(t)}var V=720*60*60,P="change",_=Buffer.from("302a300506032b6570032100","hex");function R(e){if(!e||typeof e!="object"||Array.isArray(e))return!1;let n=e;if(typeof n.signature!="string"||!n.signature||typeof n.publicKeyId!="string"||!n.publicKeyId)return!1;let r=n.payload;return!(!r||typeof r!="object"||r.v!==1||r.kind!=="chat-access-change"||typeof r.agentOsId!="string"||!r.agentOsId||typeof r.boxId!="string"||!r.boxId||typeof r.access!="string"||!r.access||!Array.isArray(r.folders)||!r.folders.every(t=>typeof t=="string")||typeof r.changedAt!="string"||Number.isNaN(Date.parse(r.changedAt))||typeof r.exp!="number"||!Number.isFinite(r.exp)||typeof r.nonce!="string"||!r.nonce)}function j(e){return!e||typeof e!="object"||Array.isArray(e)?null:e[P]??null}function K(e,n){return e.length===n.length&&e.every((r,t)=>r===n[t])}function S(e,n){let r=Date.parse(e),t=Date.parse(n);return Number.isNaN(r)?!1:Number.isNaN(t)?!0:r>t}function H(e,n,r={}){let t=r.env??process.env,a=r.nowSec??Math.floor(Date.now()/1e3);if(e==null)return{ok:!1,code:"missing",reason:"the turn carries no signed settings-change record for this raise"};if(!R(e))return{ok:!1,code:"malformed",reason:"malformed settings-change record"};let{payload:o}=e;if(o.agentOsId!==n.agentOsId)return{ok:!1,code:"mismatch",reason:"settings-change record is for another workspace"};if(o.boxId!==n.boxId)return{ok:!1,code:"mismatch",reason:"settings-change record is for another computer"};if(o.access!==n.access||!K(o.folders,n.folders))return{ok:!1,code:"mismatch",reason:"settings-change record does not match the access the turn asks for"};if(o.exp<=a)return{ok:!1,code:"expired",reason:"settings-change record expired \u2014 save the setting again in Computers settings"};let l=m(e.publicKeyId,t);if(!l)return{ok:!1,code:"no_verification_key",reason:`no verification key for publicKeyId "${e.publicKeyId}"`};let s;try{s=Buffer.from(e.signature,"base64url")}catch{return{ok:!1,code:"bad_signature",reason:"signature is not base64url"}}if(s.length!==64)return{ok:!1,code:"bad_signature",reason:"signature is not a 64-byte Ed25519 signature"};let g=!1;try{let f=Buffer.from(c(o),"utf8"),b=x({key:Buffer.concat([_,Buffer.from(l)]),format:"der",type:"spki"});g=k(null,f,b,s)}catch(f){return{ok:!1,code:"bad_signature",reason:`signature verification failed: ${f.message}`}}if(!g)return{ok:!1,code:"bad_signature",reason:"Ed25519 signature mismatch"};let i=r.lastApplied??null;return i&&(i.nonce===o.nonce||!S(o.changedAt,i.changedAt))?{ok:!1,code:"replayed",reason:"settings-change record is not newer than the one this computer last applied"}:{ok:!0,payload:o}}export{P as CHAT_ACCESS_CHANGE_RECORD_FIELD,V as CHAT_ACCESS_CHANGE_RECORD_TTL_SEC,S as isChangedAtAfter,R as isSignedChatAccessChangeRecordShape,j as readChatAccessChangeRecord,H as verifyChatAccessChangeRecord};
|
|
1
|
+
import{verify as k,createPublicKey as x}from"node:crypto";function c(e){return JSON.stringify(u(e))}function u(e){if(Array.isArray(e))return e.map(u);if(e&&typeof e=="object"){let n={};for(let r of Object.keys(e).sort())n[r]=u(e[r]);return n}return e}function d(e){if(!e)return null;let n=e.trim();if(!n)return null;if(/^[0-9a-fA-F]{64}$/.test(n))return new Uint8Array(n.match(/.{2}/g).map(r=>parseInt(r,16)));try{let r=Buffer.from(n,"base64");if(r.length===32)return new Uint8Array(r)}catch{}return null}function p(e){if(!e)return[];let n=[];for(let r of e.split(/[,\s]+/)){let t=d(r);t&&n.push(t)}return n}function y(e=process.env){if(e.NODE_ENV==="production")return!1;let n=e.KYNVER_ALLOW_DEV_SIGNING_KEYS?.trim().toLowerCase();return n==="1"||n==="true"||n==="yes"||n==="on"?!0:e.NODE_ENV==="test"||e.VITEST==="true"}import{createHash as A,createPublicKey as B,verify as O}from"node:crypto";var N=Buffer.from("302a300506032b6570032100","hex"),h="ca-6297fb8e1773",C={[h]:"16ae5cb2a61f8c94cf1b86e8258f9c2f2ff6aba5e6136250a422209b6e866881","ca-456fd128f3b3":"f29cbd559410c2578d7f9732ccd86570073355ccdb36af0d1b7dff15908466e7","ca-fc6a2e593f40":"9c0d2fec00bdfce3a448d3fb1b9b9ecfe0dad587dd37f4de492ad9ca91f53def"};function I(e){return`ca-${A("sha256").update(Buffer.from(e)).digest("hex").slice(0,12)}`}function m(e,n=process.env){let r=p(n.KYNVER_COMMAND_AUTH_PUBLIC_KEY);if(r.length>0)return r.find(a=>I(a)===e)??null;let t=C[e];return!t||e===h&&!y(n)?null:d(t)}var V=720*60*60,P="change",_=Buffer.from("302a300506032b6570032100","hex");function R(e){if(!e||typeof e!="object"||Array.isArray(e))return!1;let n=e;if(typeof n.signature!="string"||!n.signature||typeof n.publicKeyId!="string"||!n.publicKeyId)return!1;let r=n.payload;return!(!r||typeof r!="object"||r.v!==1||r.kind!=="chat-access-change"||typeof r.agentOsId!="string"||!r.agentOsId||typeof r.boxId!="string"||!r.boxId||typeof r.access!="string"||!r.access||!Array.isArray(r.folders)||!r.folders.every(t=>typeof t=="string")||typeof r.changedAt!="string"||Number.isNaN(Date.parse(r.changedAt))||typeof r.exp!="number"||!Number.isFinite(r.exp)||typeof r.nonce!="string"||!r.nonce)}function j(e){return!e||typeof e!="object"||Array.isArray(e)?null:e[P]??null}function K(e,n){return e.length===n.length&&e.every((r,t)=>r===n[t])}function S(e,n){let r=Date.parse(e),t=Date.parse(n);return Number.isNaN(r)?!1:Number.isNaN(t)?!0:r>t}function H(e,n,r={}){let t=r.env??process.env,a=r.nowSec??Math.floor(Date.now()/1e3);if(e==null)return{ok:!1,code:"missing",reason:"the turn carries no signed settings-change record for this raise"};if(!R(e))return{ok:!1,code:"malformed",reason:"malformed settings-change record"};let{payload:o}=e;if(o.agentOsId!==n.agentOsId)return{ok:!1,code:"mismatch",reason:"settings-change record is for another workspace"};if(o.boxId!==n.boxId)return{ok:!1,code:"mismatch",reason:"settings-change record is for another computer"};if(o.access!==n.access||!K(o.folders,n.folders))return{ok:!1,code:"mismatch",reason:"settings-change record does not match the access the turn asks for"};if(o.exp<=a)return{ok:!1,code:"expired",reason:"settings-change record expired \u2014 save the setting again in Computers settings"};let l=m(e.publicKeyId,t);if(!l)return{ok:!1,code:"no_verification_key",reason:`no verification key for publicKeyId "${e.publicKeyId}"`};let s;try{s=Buffer.from(e.signature,"base64url")}catch{return{ok:!1,code:"bad_signature",reason:"signature is not base64url"}}if(s.length!==64)return{ok:!1,code:"bad_signature",reason:"signature is not a 64-byte Ed25519 signature"};let g=!1;try{let f=Buffer.from(c(o),"utf8"),b=x({key:Buffer.concat([_,Buffer.from(l)]),format:"der",type:"spki"});g=k(null,f,b,s)}catch(f){return{ok:!1,code:"bad_signature",reason:`signature verification failed: ${f.message}`}}if(!g)return{ok:!1,code:"bad_signature",reason:"Ed25519 signature mismatch"};let i=r.lastApplied??null;return i&&(i.nonce===o.nonce||!S(o.changedAt,i.changedAt))?{ok:!1,code:"replayed",reason:"settings-change record is not newer than the one this computer last applied"}:{ok:!0,payload:o}}export{P as CHAT_ACCESS_CHANGE_RECORD_FIELD,V as CHAT_ACCESS_CHANGE_RECORD_TTL_SEC,S as isChangedAtAfter,R as isSignedChatAccessChangeRecordShape,j as readChatAccessChangeRecord,H as verifyChatAccessChangeRecord};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash as _,verify as E,createPublicKey as T}from"node:crypto";function i(r){return JSON.stringify(
|
|
1
|
+
import{createHash as _,verify as E,createPublicKey as T}from"node:crypto";function i(r){return JSON.stringify(l(r))}function l(r){if(Array.isArray(r))return r.map(l);if(r&&typeof r=="object"){let e={};for(let n of Object.keys(r).sort())e[n]=l(r[n]);return e}return r}function d(r){if(!r)return null;let e=r.trim();if(!e)return null;if(/^[0-9a-fA-F]{64}$/.test(e))return new Uint8Array(e.match(/.{2}/g).map(n=>parseInt(n,16)));try{let n=Buffer.from(e,"base64");if(n.length===32)return new Uint8Array(n)}catch{}return null}function h(r){if(!r)return[];let e=[];for(let n of r.split(/[,\s]+/)){let o=d(n);o&&e.push(o)}return e}function m(r=process.env){if(r.NODE_ENV==="production")return!1;let e=r.KYNVER_ALLOW_DEV_SIGNING_KEYS?.trim().toLowerCase();return e==="1"||e==="true"||e==="yes"||e==="on"?!0:r.NODE_ENV==="test"||r.VITEST==="true"}import{createHash as x,createPublicKey as j,verify as U}from"node:crypto";var M=Buffer.from("302a300506032b6570032100","hex"),b="ca-6297fb8e1773",A={[b]:"16ae5cb2a61f8c94cf1b86e8258f9c2f2ff6aba5e6136250a422209b6e866881","ca-456fd128f3b3":"f29cbd559410c2578d7f9732ccd86570073355ccdb36af0d1b7dff15908466e7","ca-fc6a2e593f40":"9c0d2fec00bdfce3a448d3fb1b9b9ecfe0dad587dd37f4de492ad9ca91f53def"};function C(r){return`ca-${x("sha256").update(Buffer.from(r)).digest("hex").slice(0,12)}`}function I(r,e=process.env){let n=h(e.KYNVER_COMMAND_AUTH_PUBLIC_KEY);if(n.length>0)return n.find(a=>C(a)===r)??null;let o=A[r];return!o||r===b&&!m(e)?null:d(o)}var Y=900,v="turnEnvelope",P=Buffer.from("302a300506032b6570032100","hex");function S(r){if(!r||typeof r!="object"||Array.isArray(r))return r;let e={...r};return delete e[v],e}function w(r){return _("sha256").update(i(S(r)),"utf8").digest("hex")}function R(r){return!r||typeof r!="object"||Array.isArray(r)?null:r[v]??null}function K(r){if(!r||typeof r!="object")return!1;let e=r;if(typeof e.signature!="string"||!e.signature||typeof e.publicKeyId!="string"||!e.publicKeyId)return!1;let n=e.payload;return!(!n||typeof n!="object"||n.v!==1||n.kind!=="chat-turn"||typeof n.turnId!="string"||!n.turnId||typeof n.agentOsId!="string"||!n.agentOsId||n.userId!==null&&(typeof n.userId!="string"||!n.userId)||n.boxId!==null&&typeof n.boxId!="string"||n.access!==null&&(!n.access||typeof n.access!="object"||Array.isArray(n.access))||typeof n.payloadHash!="string"||!/^[0-9a-f]{64}$/.test(n.payloadHash)||typeof n.exp!="number"||!Number.isFinite(n.exp)||typeof n.nonce!="string"||!n.nonce)}function N(r,e){let n=r&&typeof r=="object"&&!Array.isArray(r)?r.computerAccess??null:null;return n===null&&e===null?!0:n===null||e===null?!1:i(n)===i(e)}var k=class{constructor(e=1e4){this.maxEntries=e}maxEntries;byNonce=new Map;byTurn=new Map;consume(e){let n=e.nowSec??Math.floor(Date.now()/1e3);this.purge(n);let o=this.byNonce.get(e.nonce);if(o)return o.turnId!==e.turnId?{ok:!1,replay:p(o,"turn_mismatch")}:o.state==="verified"?{ok:!0,redelivered:!0}:{ok:!1,replay:p(o,o.state==="started"?"in_flight":"already_ran")};let a=this.byTurn.get(e.turnId);if(a){if(a.state!=="verified")return{ok:!1,replay:p(a,a.state==="started"?"in_flight":"already_ran")};this.byNonce.delete(a.nonce);let t={...a,nonce:e.nonce,untilSec:Math.max(a.untilSec,e.expSec+60)};return this.byNonce.set(e.nonce,t),this.byTurn.set(e.turnId,t),{ok:!0,redelivered:!0}}if(this.byNonce.size>=this.maxEntries){let t=this.byNonce.values().next().value;t&&this.forget(t)}let s={nonce:e.nonce,turnId:e.turnId,firstSeenSec:n,state:"verified",untilSec:e.expSec+60};return this.byNonce.set(e.nonce,s),this.byTurn.set(e.turnId,s),{ok:!0,redelivered:!1}}markStarted(e){let n=this.byTurn.get(e);return n?(n.state==="verified"&&(n.state="started"),!0):!1}markFinished(e){let n=this.byTurn.get(e);return n?(n.state="finished",!0):!1}stateOf(e){return this.byTurn.get(e)?.state??null}purge(e=Math.floor(Date.now()/1e3)){for(let n of[...this.byNonce.values()])n.untilSec<=e&&this.forget(n)}forget(e){this.byNonce.delete(e.nonce),this.byTurn.get(e.turnId)===e&&this.byTurn.delete(e.turnId)}get size(){return this.byNonce.size}};function p(r,e){return{state:e,firstSeenAt:new Date(r.firstSeenSec*1e3).toISOString(),turnId:r.turnId}}function B(r){return T({key:Buffer.concat([P,Buffer.from(r)]),format:"der",type:"spki"})}function J(r,e,n={}){let o=n.env??process.env,a=n.nowSec??Math.floor(Date.now()/1e3),s=R(r);if(s===null)return{ok:!1,code:"unsigned",reason:"turn carries no signed envelope"};if(!K(s))return{ok:!1,code:"malformed",reason:"malformed chat-turn envelope"};let t=s;if(t.payload.agentOsId!==e.agentOsId)return{ok:!1,code:"agent_os_mismatch",reason:"envelope is for another workspace"};if(t.payload.turnId!==e.turnId)return{ok:!1,code:"turn_id_mismatch",reason:"envelope is for another turn"};if(t.payload.exp<=a)return{ok:!1,code:"expired",reason:"envelope expired"};let y=I(t.publicKeyId,o);if(!y)return{ok:!1,code:"no_verification_key",reason:`no verification key for publicKeyId "${t.publicKeyId}"`};let f;try{f=Buffer.from(t.signature,"base64url")}catch{return{ok:!1,code:"bad_signature",reason:"signature is not base64url"}}if(f.length!==64)return{ok:!1,code:"bad_signature",reason:"signature is not a 64-byte Ed25519 signature"};let g=!1;try{let c=Buffer.from(i(t.payload),"utf8");g=E(null,c,B(y),f)}catch(c){return{ok:!1,code:"bad_signature",reason:`signature verification failed: ${c.message}`}}if(!g)return{ok:!1,code:"bad_signature",reason:"Ed25519 signature mismatch"};if(w(r)!==t.payload.payloadHash)return{ok:!1,code:"payload_tampered",reason:"turn payload does not match its signed hash"};if(!N(r,t.payload.access))return{ok:!1,code:"access_mismatch",reason:"computer access on the turn differs from the signed setting"};if(!n.nonces)return{ok:!0,payload:t.payload,redelivered:!1};let u=n.nonces.consume({nonce:t.payload.nonce,turnId:t.payload.turnId,expSec:t.payload.exp,nowSec:a});return u.ok?{ok:!0,payload:t.payload,redelivered:u.redelivered}:{ok:!1,code:"replayed",reason:`envelope nonce already used on this computer (${O(u.replay)})`,replay:u.replay}}function O(r){return`${r.state==="in_flight"?"this computer is running that turn now":r.state==="already_ran"?"that turn already ran here":`the nonce belongs to turn ${r.turnId}`}; first seen ${r.firstSeenAt}`}export{v as CHAT_TURN_ENVELOPE_FIELD,Y as CHAT_TURN_ENVELOPE_TTL_SEC,k as ChatTurnNonceCache,N as chatTurnAccessMatches,w as hashChatTurnPayload,K as isSignedChatTurnEnvelopeShape,R as readChatTurnEnvelope,S as stripChatTurnEnvelope,J as verifyChatTurnEnvelope};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash as h,createPublicKey as b,verify as c}from"node:crypto";function u(e){return JSON.stringify(s(e))}function s(e){if(Array.isArray(e))return e.map(s);if(e&&typeof e=="object"){let n={};for(let r of Object.keys(e).sort())n[r]=s(e[r]);return n}return e}function f(e){if(!e)return null;let n=e.trim();if(!n)return null;if(/^[0-9a-fA-F]{64}$/.test(n))return new Uint8Array(n.match(/.{2}/g).map(r=>parseInt(r,16)));try{let r=Buffer.from(n,"base64");if(r.length===32)return new Uint8Array(r)}catch{}return null}function l(e){if(!e)return[];let n=[];for(let r of e.split(/[,\s]+/)){let t=f(r);t&&n.push(t)}return n}function d(e=process.env){if(e.NODE_ENV==="production")return!1;let n=e.KYNVER_ALLOW_DEV_SIGNING_KEYS?.trim().toLowerCase();return n==="1"||n==="true"||n==="yes"||n==="on"?!0:e.NODE_ENV==="test"||e.VITEST==="true"}var E=120,I=Buffer.from("302a300506032b6570032100","hex"),p="ca-6297fb8e1773",A={[p]:"16ae5cb2a61f8c94cf1b86e8258f9c2f2ff6aba5e6136250a422209b6e866881","ca-456fd128f3b3":"f29cbd559410c2578d7f9732ccd86570073355ccdb36af0d1b7dff15908466e7"};function P(e){return`ca-${h("sha256").update(Buffer.from(e)).digest("hex").slice(0,12)}`}function y(e,n=process.env){let r=l(n.KYNVER_COMMAND_AUTH_PUBLIC_KEY);if(r.length>0)return r.find(o=>P(o)===e)??null;let t=A[e];return!t||e===p&&!d(n)?null:f(t)}function g(e){return b({key:Buffer.concat([I,Buffer.from(e)]),format:"der",type:"spki"})}function k(e,n){return!Array.isArray(e)||e.length!==n.length?!1:e.every((r,t)=>typeof r=="string"&&r===n[t])}function x(e){if(!e||typeof e!="object")return!1;let n=e;if(typeof n.signature!="string"||!n.signature||typeof n.publicKeyId!="string"||!n.publicKeyId)return!1;let r=n.payload;return!(!r||typeof r!="object"||r.v!==1||typeof r.agentOsId!="string"||!r.agentOsId||!Array.isArray(r.argv)||r.argv.some(t=>typeof t!="string")||typeof r.userId!="string"||!r.userId||typeof r.exp!="number"||!Number.isFinite(r.exp)||typeof r.nonce!="string"||!r.nonce)}function O(e,n,r=process.env,t=Math.floor(Date.now()/1e3)){if(!x(e))return{ok:!1,reason:"malformed command authorization proof"};if(e.payload.agentOsId!==n.agentOsId)return{ok:!1,reason:"agentOsId mismatch"};if(!k(e.payload.argv,n.argv))return{ok:!1,reason:"argv mismatch"};if(e.payload.exp<=t)return{ok:!1,reason:"proof expired"};let o=y(e.publicKeyId,r);if(!o)return{ok:!1,reason:`no verification key for publicKeyId "${e.publicKeyId}"`};let i;try{i=Buffer.from(e.signature,"base64url")}catch{return{ok:!1,reason:"signature is not base64url"}}if(i.length!==64)return{ok:!1,reason:"signature is not a 64-byte Ed25519 signature"};try{let a=Buffer.from(u(e.payload),"utf8");return c(null,a,g(o),i)?{ok:!0,payload:e.payload}:{ok:!1,reason:"Ed25519 signature mismatch"}}catch(a){return{ok:!1,reason:`signature verification failed: ${a.message}`}}}var R=120;function C(e){if(!e||typeof e!="object")return!1;let n=e;if(typeof n.signature!="string"||!n.signature||typeof n.publicKeyId!="string"||!n.publicKeyId)return!1;let r=n.payload;return!(!r||typeof r!="object"||r.v!==1||r.kind!=="chat"||typeof r.agentOsId!="string"||!r.agentOsId||typeof r.exp!="number"||!Number.isFinite(r.exp)||typeof r.nonce!="string"||!r.nonce)}function w(e,n,r=process.env,t=Math.floor(Date.now()/1e3)){if(!C(e))return{ok:!1,reason:"malformed chat-turn authorization proof"};if(e.payload.agentOsId!==n.agentOsId)return{ok:!1,reason:"agentOsId mismatch"};if(e.payload.exp<=t)return{ok:!1,reason:"proof expired"};let o=y(e.publicKeyId,r);if(!o)return{ok:!1,reason:`no verification key for publicKeyId "${e.publicKeyId}"`};let i;try{i=Buffer.from(e.signature,"base64url")}catch{return{ok:!1,reason:"signature is not base64url"}}if(i.length!==64)return{ok:!1,reason:"signature is not a 64-byte Ed25519 signature"};try{let a=Buffer.from(u(e.payload),"utf8");return c(null,a,g(o),i)?{ok:!0,payload:e.payload}:{ok:!1,reason:"Ed25519 signature mismatch"}}catch(a){return{ok:!1,reason:`signature verification failed: ${a.message}`}}}export{R as CHAT_TURN_AUTH_PROOF_TTL_SEC,p as COMMAND_AUTH_DEV_FALLBACK_PUBLIC_KEY_ID,E as COMMAND_AUTH_PROOF_TTL_SEC,A as PINNED_COMMAND_AUTH_PUBLIC_KEYS,k as argvMatchesProof,P as deriveCommandAuthKeyId,C as isSignedChatTurnAuthProofShape,x as isSignedCommandAuthProofShape,y as resolveCommandAuthPublicKey,w as verifyChatTurnAuthorizationProof,O as verifyCommandAuthorizationProof};
|
|
1
|
+
import{createHash as h,createPublicKey as b,verify as c}from"node:crypto";function u(e){return JSON.stringify(s(e))}function s(e){if(Array.isArray(e))return e.map(s);if(e&&typeof e=="object"){let n={};for(let r of Object.keys(e).sort())n[r]=s(e[r]);return n}return e}function f(e){if(!e)return null;let n=e.trim();if(!n)return null;if(/^[0-9a-fA-F]{64}$/.test(n))return new Uint8Array(n.match(/.{2}/g).map(r=>parseInt(r,16)));try{let r=Buffer.from(n,"base64");if(r.length===32)return new Uint8Array(r)}catch{}return null}function l(e){if(!e)return[];let n=[];for(let r of e.split(/[,\s]+/)){let t=f(r);t&&n.push(t)}return n}function d(e=process.env){if(e.NODE_ENV==="production")return!1;let n=e.KYNVER_ALLOW_DEV_SIGNING_KEYS?.trim().toLowerCase();return n==="1"||n==="true"||n==="yes"||n==="on"?!0:e.NODE_ENV==="test"||e.VITEST==="true"}var E=120,I=Buffer.from("302a300506032b6570032100","hex"),p="ca-6297fb8e1773",A={[p]:"16ae5cb2a61f8c94cf1b86e8258f9c2f2ff6aba5e6136250a422209b6e866881","ca-456fd128f3b3":"f29cbd559410c2578d7f9732ccd86570073355ccdb36af0d1b7dff15908466e7","ca-fc6a2e593f40":"9c0d2fec00bdfce3a448d3fb1b9b9ecfe0dad587dd37f4de492ad9ca91f53def"};function P(e){return`ca-${h("sha256").update(Buffer.from(e)).digest("hex").slice(0,12)}`}function y(e,n=process.env){let r=l(n.KYNVER_COMMAND_AUTH_PUBLIC_KEY);if(r.length>0)return r.find(o=>P(o)===e)??null;let t=A[e];return!t||e===p&&!d(n)?null:f(t)}function g(e){return b({key:Buffer.concat([I,Buffer.from(e)]),format:"der",type:"spki"})}function k(e,n){return!Array.isArray(e)||e.length!==n.length?!1:e.every((r,t)=>typeof r=="string"&&r===n[t])}function x(e){if(!e||typeof e!="object")return!1;let n=e;if(typeof n.signature!="string"||!n.signature||typeof n.publicKeyId!="string"||!n.publicKeyId)return!1;let r=n.payload;return!(!r||typeof r!="object"||r.v!==1||typeof r.agentOsId!="string"||!r.agentOsId||!Array.isArray(r.argv)||r.argv.some(t=>typeof t!="string")||typeof r.userId!="string"||!r.userId||typeof r.exp!="number"||!Number.isFinite(r.exp)||typeof r.nonce!="string"||!r.nonce)}function O(e,n,r=process.env,t=Math.floor(Date.now()/1e3)){if(!x(e))return{ok:!1,reason:"malformed command authorization proof"};if(e.payload.agentOsId!==n.agentOsId)return{ok:!1,reason:"agentOsId mismatch"};if(!k(e.payload.argv,n.argv))return{ok:!1,reason:"argv mismatch"};if(e.payload.exp<=t)return{ok:!1,reason:"proof expired"};let o=y(e.publicKeyId,r);if(!o)return{ok:!1,reason:`no verification key for publicKeyId "${e.publicKeyId}"`};let i;try{i=Buffer.from(e.signature,"base64url")}catch{return{ok:!1,reason:"signature is not base64url"}}if(i.length!==64)return{ok:!1,reason:"signature is not a 64-byte Ed25519 signature"};try{let a=Buffer.from(u(e.payload),"utf8");return c(null,a,g(o),i)?{ok:!0,payload:e.payload}:{ok:!1,reason:"Ed25519 signature mismatch"}}catch(a){return{ok:!1,reason:`signature verification failed: ${a.message}`}}}var R=120;function C(e){if(!e||typeof e!="object")return!1;let n=e;if(typeof n.signature!="string"||!n.signature||typeof n.publicKeyId!="string"||!n.publicKeyId)return!1;let r=n.payload;return!(!r||typeof r!="object"||r.v!==1||r.kind!=="chat"||typeof r.agentOsId!="string"||!r.agentOsId||typeof r.exp!="number"||!Number.isFinite(r.exp)||typeof r.nonce!="string"||!r.nonce)}function w(e,n,r=process.env,t=Math.floor(Date.now()/1e3)){if(!C(e))return{ok:!1,reason:"malformed chat-turn authorization proof"};if(e.payload.agentOsId!==n.agentOsId)return{ok:!1,reason:"agentOsId mismatch"};if(e.payload.exp<=t)return{ok:!1,reason:"proof expired"};let o=y(e.publicKeyId,r);if(!o)return{ok:!1,reason:`no verification key for publicKeyId "${e.publicKeyId}"`};let i;try{i=Buffer.from(e.signature,"base64url")}catch{return{ok:!1,reason:"signature is not base64url"}}if(i.length!==64)return{ok:!1,reason:"signature is not a 64-byte Ed25519 signature"};try{let a=Buffer.from(u(e.payload),"utf8");return c(null,a,g(o),i)?{ok:!0,payload:e.payload}:{ok:!1,reason:"Ed25519 signature mismatch"}}catch(a){return{ok:!1,reason:`signature verification failed: ${a.message}`}}}export{R as CHAT_TURN_AUTH_PROOF_TTL_SEC,p as COMMAND_AUTH_DEV_FALLBACK_PUBLIC_KEY_ID,E as COMMAND_AUTH_PROOF_TTL_SEC,A as PINNED_COMMAND_AUTH_PUBLIC_KEYS,k as argvMatchesProof,P as deriveCommandAuthKeyId,C as isSignedChatTurnAuthProofShape,x as isSignedCommandAuthProofShape,y as resolveCommandAuthPublicKey,w as verifyChatTurnAuthorizationProof,O as verifyCommandAuthorizationProof};
|