Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ is `skillhook`. User docs: `README.md`, `docs/`, `llms.txt`.
| `src/events.ts` | The in-process event bus (`Events`, `EventMap`): the queue publishes `job.*`, the scheduler `schedule.*`, the registry `skill.changed`, `serve` `server.*`; `GET /events` and `GET /jobs/<id>/events` stream it (SSE, `openEventStream` in `src/server.ts`). The cloud link will subscribe to the same bus. |
| `src/progress.ts`, `src/answer.ts`, `src/mcp-job.ts`, `src/commands/job.ts`, `src/reporting.ts` | The job API for the running agent and the human loop. `progress.ts` is the file model in the job directory (`progress.jsonl`, `progress.json`, `question.json`, `answer.json`) that the queue watches; `reporting.ts` normalizes what a report says for people (a job title, a one-line headline, typed links, choices) the same way for every front end; `mcp-job.ts` serves it as the per-run MCP server (`skillhook mcp --job`, injected by the runners) and `commands/job.ts` as `skillhook job progress\|ask\|outcome\|note\|context`; `answer.ts` (leaf, like `manual.ts`) delivers a person's answer live or as a `trigger: resume` job that reopens the session. |
| `src/runners/` | `claude.ts`, `codex.ts`, `shell.ts`: build argv, parse output; `env.ts` is the env allow-list (`baseRunEnv` is also what probes run with); `failure.ts` classifies a failed run (`failure.kind`, from the CLIs' captured lines) and holds the `fallback` / `retry` schemas. |
| `src/cloud/` | The Skillhook Cloud side of this machine (`control.ts`: the commands that act on it, with the config keys the cloud may never change; `seal.ts`: X25519 + AES-GCM sealing for secrets). `protocol.ts` is the wire protocol as pure zod (no `node:` imports; exported as `@meterapp/skillhook/protocol`, the cloud repo imports it), with the vocabulary repeated as literals and a drift test; `config.ts` holds the URL rules, the kill switch and `commandAllowed`; `link.ts` is the sync loop `serve` runs (idle until `cloud.enabled`), `outbox.ts` the spool and ledgers in `jobs/.cloud/`, `redact.ts` what is removed before anything leaves, `commands.ts` the command dispatcher, `ingress.ts` hosted deliveries replayed to the local server, `pair.ts` / `src/commands/cloud.ts` pairing, `report.ts` a person's problem report (`cloud report`, MCP `cloud_report_issue`) with the machine's diagnostics, `api.ts` the client for the cloud's public API with an organisation API key (`cloud login|machines|jobs|job`), `tools.ts` the cloud's tool catalogue (`GET /api/v1/tools`) and its calls, mapped to CLI arguments (`cloud tools`, `cloud <tool>`, `cloud overview`), `bridge.ts` the same catalogue as the `skillhook-cloud` MCP server (`skillhook mcp --cloud`), `remote-secret.ts` a secret generated on another machine and sealed to a one-time key pair here (`cloud secret`, MCP `generate_secret`). The cloud owns that catalogue: a tool it adds needs no change here, so never hand-write a cloud tool in skillhook. Tests talk to `src/test-support/fake-cloud.ts`, never to a real cloud (and `fake-server.ts` stands in for a running server). |
| `src/cloud/` | The Skillhook Cloud side of this machine (`control.ts`: the commands that act on it, with the config keys the cloud may never change; `seal.ts`: X25519 + AES-GCM sealing for secrets). `protocol.ts` is the wire protocol as pure zod (no `node:` imports; exported as `@meterapp/skillhook/protocol`, the cloud repo imports it), with the vocabulary repeated as literals and a drift test; `config.ts` holds the URL rules, the kill switch and `commandAllowed`; `link.ts` is the sync loop `serve` runs (idle until `cloud.enabled`), `outbox.ts` the spool and ledgers in `jobs/.cloud/`, `redact.ts` what is removed before anything leaves, `commands.ts` the command dispatcher, `ingress.ts` hosted deliveries replayed to the local server, `pair.ts` / `src/commands/cloud.ts` pairing, `report.ts` a person's problem report (`cloud report`, MCP `cloud_report_issue`) with the machine's diagnostics, `api.ts` the client for the cloud's public API with an organisation API key (`cloud login|machines|jobs|job`), `login.ts` signing in with the browser to get one (RFC 8628's device flow: `cloud login`, and `skillhook_cloud_setup` of `mcp --cloud`), `tools.ts` the cloud's tool catalogue (`GET /api/v1/tools`) and its calls, mapped to CLI arguments (`cloud tools`, `cloud <tool>`, `cloud overview`), `bridge.ts` the same catalogue as the `skillhook-cloud` MCP server (`skillhook mcp --cloud`), `remote-secret.ts` a secret generated on another machine and sealed to a one-time key pair here (`cloud secret`, MCP `generate_secret`). The cloud owns that catalogue: a tool it adds needs no change here, so never hand-write a cloud tool in skillhook. Tests talk to `src/test-support/fake-cloud.ts`, never to a real cloud (and `fake-server.ts` stands in for a running server). |
| `src/stats.ts` | Pure aggregation over job records and delivery records (`computeStats`) and `collectStats` over the store and the log: `GET /stats`, `skillhook stats`, MCP `get_stats`. New numbers go here with a unit test on synthetic records. |
| `src/readiness.ts` | Is a runner installed and logged in (`checkReadiness`, `ReadinessCache`): the queue's pre-flight before every job, `GET /runners`, `skillhook runners`, `runners.changed`. A not-ready runner fails the job fast or hands it to a `fallback:` runner; a failed run may be retried or handed over only before the agent produced anything. |
| `src/ops.ts` | Shared operations (create skill, run locally, sign+send, resolve URLs). CLI and MCP both call this; do not duplicate logic in either. |
Expand Down Expand Up @@ -62,7 +62,7 @@ Runtime state lives outside the repo in `~/.skillhook` (`SKILLHOOK_HOME`):
- **Every CLI command supports `--json`** and returns non-zero on failure. Register new commands in `COMMANDS` (with the usage text) and `HELP` in `src/commands/main.ts`, then in the README table. `--help` / `-h` never reaches a command: `main` prints the usage from `COMMANDS` instead, so a command never checks for it; a new subcommand gets a line in the `--help` test of `src/cli.test.ts`.
- **Third-party facts** (Granola, Sentry, GitHub, Tailscale) are stated in `docs/` and the examples with the exact header names; change them only with a source.
- **Tests are hermetic**: `tempHome()` from `src/test-support/helpers.ts`, fake runners, ephemeral ports. Never touch `~/.skillhook`, the real `claude`/`codex`, `launchctl` or `tailscale` from a test. Never reach the real npm registry either: point `SKILLHOOK_NPM_REGISTRY` at a local `node:http` server or set `SKILLHOOK_NO_UPDATE_CHECK=1`. Nor Skillhook Cloud: `cloud.url` defaults to the live service (`DEFAULT_CLOUD_URL`), so a test gives a `FakeCloud` URL (`SKILLHOOK_CLOUD_URL`, `cloud.url`, `--url`), or answers what is sent to the default itself (a `fetchImpl`, or a stubbed `fetch` that hands it to a `FakeCloud`).
- **Outbound requests are opt-in and enumerated.** By default the CLI phones home at most once an hour, and only for updates (`src/update.ts`: the registry's `latest` dist-tag, cached 1 h, from a detached process so no command waits, never in CI or when `SKILLHOOK_NO_UPDATE_CHECK` / `update_check: false` say so); with `auto_update` (the default) that process also installs a newer version of skillhook itself, from the same registry, with the package manager that installed it, and nothing else. The one other outbound connection is the Skillhook Cloud link (`src/cloud/link.ts`), and only after `skillhook cloud connect` wrote `cloud.enabled` and `SKILLHOOK_CLOUD_TOKEN`: it talks to `cloud.url` over HTTPS only, sends only what `docs/cloud.md` lists (redacted, scrubbed of every `.env` value), obeys `cloud.mode` and the local allow/deny lists (which the cloud cannot change), and stops on `cloud.enabled: false`, `SKILLHOOK_NO_CLOUD=1` or `cloud disconnect`. Never enable it by default, from `init` or from a job. Besides the link, the person-invoked cloud commands send one request each, only when run, only to the machine's cloud URL (`cloud.url` or `SKILLHOOK_CLOUD_URL`, HTTPS only): `cloud connect` / `disconnect` (pairing, revocation), `cloud report` and the MCP tool `cloud_report_issue` (`src/cloud/report.ts`: the person's text plus the diagnostics `docs/cloud.md` lists, scrubbed like the link's uploads, with the machine token), and `cloud login|overview|machines|jobs|job|tools|<tool>|secret` and the tools of `skillhook mcp --cloud` (`src/cloud/api.ts`, `tools.ts`, `bridge.ts`, `remote-secret.ts`: requests with the person's organisation API key `SKILLHOOK_CLOUD_API_KEY`, never with the machine token, only to the cloud the key was checked against at login (`SKILLHOOK_CLOUD_API_URL`), each one a command the person ran or a tool their agent called; `skillhook mcp --cloud` also reads the catalogue once when it starts; skillhook's own tools do not move them for an agent: the local MCP refuses `cloud.*` settings and `SKILLHOOK_CLOUD_*` secrets, and no skill's `secret_env` may name skillhook's own credentials; an agent that runs commands acts with the person's rights all the same); the last two groups refuse under `SKILLHOOK_NO_CLOUD=1`. Do not add other outbound requests the user did not ask for, and never install anything automatically but skillhook's own update.
- **Outbound requests are opt-in and enumerated.** By default the CLI phones home at most once an hour, and only for updates (`src/update.ts`: the registry's `latest` dist-tag, cached 1 h, from a detached process so no command waits, never in CI or when `SKILLHOOK_NO_UPDATE_CHECK` / `update_check: false` say so); with `auto_update` (the default) that process also installs a newer version of skillhook itself, from the same registry, with the package manager that installed it, and nothing else. The one other outbound connection is the Skillhook Cloud link (`src/cloud/link.ts`), and only after `skillhook cloud connect` wrote `cloud.enabled` and `SKILLHOOK_CLOUD_TOKEN`: it talks to `cloud.url` over HTTPS only, sends only what `docs/cloud.md` lists (redacted, scrubbed of every `.env` value), obeys `cloud.mode` and the local allow/deny lists (which the cloud cannot change), and stops on `cloud.enabled: false`, `SKILLHOOK_NO_CLOUD=1` or `cloud disconnect`. Never enable it by default, from `init` or from a job. Besides the link, the person-invoked cloud commands send one request each, only when run, only to the machine's cloud URL (`cloud.url` or `SKILLHOOK_CLOUD_URL`, HTTPS only): `cloud connect` / `disconnect` (pairing, revocation), `cloud report` and the MCP tool `cloud_report_issue` (`src/cloud/report.ts`: the person's text plus the diagnostics `docs/cloud.md` lists, scrubbed like the link's uploads, with the machine token), and `cloud login|overview|machines|jobs|job|tools|<tool>|secret` and the tools of `skillhook mcp --cloud` (`src/cloud/api.ts`, `tools.ts`, `bridge.ts`, `remote-secret.ts`: requests with the person's organisation API key `SKILLHOOK_CLOUD_API_KEY`, never with the machine token, only to the cloud the key was checked against at login (`SKILLHOOK_CLOUD_API_URL`), each one a command the person ran or a tool their agent called; `cloud login` without `--key`, and `skillhook_cloud_setup` without a working key, sign in with the browser instead (`src/cloud/login.ts`): `POST /api/auth/device/code` with this computer's name, then `POST /api/auth/device/token` with the device code every few seconds until a person approves it on the cloud, ten minutes at most, and the page opened with the system's opener (argv, no shell; not over SSH or without a display), the key it gets kept like a pasted one; `skillhook mcp --cloud` also reads the catalogue once when it starts; skillhook's own tools do not move them for an agent: the local MCP refuses `cloud.*` settings and `SKILLHOOK_CLOUD_*` secrets, and no skill's `secret_env` may name skillhook's own credentials; an agent that runs commands acts with the person's rights all the same); the last two groups refuse under `SKILLHOOK_NO_CLOUD=1`. Do not add other outbound requests the user did not ask for, and never install anything automatically but skillhook's own update.

## Checks

Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to skillhook, newest first. The format follows [Keep a Chang

## Unreleased

- `skillhook cloud login` signs in with the browser: it shows a code, opens the cloud's sign-in page, and the person
approves it there (choosing the organisation, where they are an admin or owner, and the access: read, run or
admin); the cloud makes a new organisation API key for this computer,
named `skillhook CLI on <hostname>` under Settings → API keys, and hands it to the waiting CLI once. Nobody copies a
key out of the dashboard and into a terminal any more. `--no-browser` only prints the link; the link and the code go
to stderr, so `--json` still answers on stdout. `--key shc_…`, `--key -` (stdin) and `--key` alone (asked for at a
terminal) keep a key made on the dashboard, as before; in CI, where nobody can approve, login says to use them.
Needs a Skillhook Cloud with browser sign-in (`/api/auth/device/*`); an older one says to use `--key`.
- `skillhook_cloud_setup` of `skillhook mcp --cloud` does the same for an agent: without a working key it opens the
sign-in page in the person's browser and returns the link and the code to show them, keeps the key once they
approved and loads the cloud's tools; `wait_seconds` waits for the approval. The key never passes through the
conversation, and the sign-in goes only to the cloud the key would.
- The messages that sent people to Settings → API keys to make a key first (no key, a refused key, a missing scope)
say to run `skillhook cloud login` instead.

## 0.9.1 (2026-10-07)

- `skillhook init` names pairing with Skillhook Cloud (https://skillhook.dev) among its next steps, and the `cloud link`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ Agents reading this repository should start with [`AGENTS.md`](AGENTS.md) (layou
| `skillhook mcp [--print-config]` · `mcp --job` | MCP server over stdio; `--print-config` prints client configuration; `--job` serves one run's job API (the runners start it). |
| `skillhook cloud connect --code XXXX-XXXX [--control] [--url U]` · `cloud status` · `cloud disconnect [--keep-token]` | Pair this machine with [Skillhook Cloud](https://skillhook.dev) (opt-in, outbound only; observe mode unless `--control`): webhooks, jobs, health and stats of every machine in one place, hosted webhook URLs. See [Skillhook Cloud](#skillhook-cloud) and [docs/cloud.md](docs/cloud.md). |
| `skillhook cloud report "<title>" [--body T\|--body-file F\|--body -] [--kind bug\|question\|feature\|other] [--severity low\|normal\|high\|urgent] [--job ID] [--delivery ID] [--skill S] [--email E] [--no-diagnostics] [--dry-run]` | Report a problem to the Skillhook team from a paired machine, with the diagnostics it already has (versions, link state, runner readiness, failing checks; scrubbed of every `.env` value, never payloads or logs; `--dry-run` shows the JSON). See [docs/cloud.md](docs/cloud.md#reporting-a-problem). |
| `skillhook cloud login [--url U] [--key shc_…\|-]` · `cloud logout` | Keep an organisation API key (Settings → API keys on the dashboard; asked for at a terminal) in `.env` as `SKILLHOOK_CLOUD_API_KEY`, never printed, never the machine token; the cloud is Skillhook Cloud (`https://skillhook.dev`) unless `--url` names another deployment. |
| `skillhook cloud login [--url U] [--no-browser]` · `cloud login --key [shc_…\|-]` · `cloud logout` | Sign in with the browser: approve the code it shows on the cloud, and the organisation API key the cloud makes for this computer is kept in `.env` as `SKILLHOOK_CLOUD_API_KEY`, never printed, never the machine token (`--key` keeps one from Settings → API keys instead); the cloud is Skillhook Cloud (`https://skillhook.dev`) unless `--url` names another deployment. |
| `skillhook cloud overview` · `cloud machines` · `cloud jobs [--machine M] [--skill S] [--status ST] [--outcome O] [--waiting] [--limit N] [--before C]` · `cloud job <id>` | With the key: what needs a person across the organisation (waiting agents, alerts, failing checks, failures, the day's numbers); its machines and jobs. |
| `skillhook cloud tools [tool]` · `cloud <tool> [args] [--param value]…` · `cloud secret <machine> <skill\|NAME> [--force]` | Every tool the cloud offers the key (the dashboard's views and actions: answer agents, replay, run and test skills, stats, alerts, hosted URLs, skills, machines…), run by name; a skill's secret generated on a machine and opened only here. See [docs/cloud.md](docs/cloud.md#the-whole-organisation-with-an-api-key). |
| `skillhook config show\|get <key>\|set <key> <value>\|unset <key>\|reload\|path` | Read and edit `skillhook.json`; `set`/`unset` tell the running server, which applies every key but `host` and `port` live. |
Expand Down
Loading
Loading