Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6db4e87
feat(ENG-967): add arkor dev --agent mode for coding agents
k-taro56 Jul 22, 2026
ca19a4e
fix(ENG-967): harden agent-mode cleanup and Windows test guards (self…
k-taro56 Jul 22, 2026
1692adb
fix(ENG-967): address code-review findings for arkor dev --agent
k-taro56 Jul 23, 2026
28c64cc
fix(ENG-967): second-review fixes for arkor dev --agent
k-taro56 Jul 23, 2026
82c54fd
fix(ENG-967): make the relative-cwd probe test load-bearing + reconci…
k-taro56 Jul 23, 2026
0093825
fix(ENG-967): exhaustive-audit fixes (test effectiveness, agent url, …
k-taro56 Jul 23, 2026
49b706b
fix(ENG-967): audit rounds 2-3 (probe hardening, port validation, loa…
k-taro56 Jul 23, 2026
e421dad
fix(ENG-967): audit round 4 (probe-timeout test, coverage, JSDoc)
k-taro56 Jul 23, 2026
1428227
fix(ENG-967): audit round 5 (CSRF-doc carve-out, bin.ts coverage, por…
k-taro56 Jul 23, 2026
d4e123c
test(ENG-967): harden agent-mode probe/bootstrap tests and status con…
k-taro56 Jul 23, 2026
fc84107
test(ENG-967): fix probe-timeout test to be honest and fast (self-rev…
k-taro56 Jul 23, 2026
9719fe7
Merge remote-tracking branch 'origin/main' into eng-967
k-taro56 Jul 24, 2026
dd5e221
fix(ENG-967): self-review r8 (doc precision, JSDoc steering, close co…
k-taro56 Jul 24, 2026
9240088
fix(ENG-967): assert adopt-path server.close(), correct redirect comm…
k-taro56 Jul 26, 2026
cc144c4
docs(ENG-967): document the deferred credential bootstrap, add /api/s…
k-taro56 Jul 26, 2026
1739c67
docs(e2e): correct the waitForPort bind-ordering comment (self-review…
k-taro56 Jul 26, 2026
f136dc3
Merge remote-tracking branch 'origin/main' into eng-967
k-taro56 Jul 26, 2026
8fa99a7
fix(ENG-967): full-scope audit round 13 (pre-existing issues now in s…
k-taro56 Jul 26, 2026
565b7b1
fix(ENG-967): full-scope audit round 14
k-taro56 Jul 26, 2026
3a7e289
docs(ENG-967): correct the Studio dev-loop, shared-machine and bootst…
k-taro56 Aug 1, 2026
682cab8
fix(ENG-967): full-scope audit round 16
k-taro56 Aug 2, 2026
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
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ cd my-arkor-app && pnpm dev # Studio at http://127.0.

`arkor dev` generates a 32-byte base64url token per launch ([packages/arkor/src/cli/commands/dev.ts](packages/arkor/src/cli/commands/dev.ts)) and:

1. Passes it to `buildStudioApp({ studioToken })`. The Hono server validates every `/api/*` request via `X-Arkor-Studio-Token` header (or `?studioToken=` query for `EventSource`, which can't set headers). Comparison uses `timingSafeEqual`.
1. Passes it to `buildStudioApp({ studioToken })`. The Hono server validates every `/api/*` request via `X-Arkor-Studio-Token` header (or `?studioToken=` query for `EventSource`, which can't set headers). Comparison uses `timingSafeEqual`. **One carve-out:** `GET /api/status` is token-exempt (still behind the loopback + Host guard) because it is secrets-free and never executes user code; this lets the port-collision probe confirm an occupant without transmitting the token to an unverified peer (see item 4).
2. Persists it to `~/.arkor/studio-token` (mode 0600) so the SPA dev workflow (`pnpm --filter @arkor/studio-app dev`) can read it via the `arkor-studio-token` Vite plugin in [packages/studio-app/vite.config.ts](packages/studio-app/vite.config.ts), which injects `<meta name="arkor-studio-token">` into `index.html` on each request. Persistence failure must NOT block server start (read-only `$HOME` on Docker, etc.); just warn.
3. Cleans up on `exit`/SIGINT/SIGTERM/SIGHUP via `unlinkSync`, after verifying the file still holds this instance's token (a second `arkor dev` on another port may have overwritten it; last writer wins and the previous owner must not delete it). Signal handlers exit with the conventional `128 + signal` code (SIGINT -> 130, SIGTERM -> 143, SIGHUP -> 129) so supervisors can distinguish signal termination from a clean exit.
4. **Agent mode (ENG-967).** `arkor dev --agent` additionally writes the token as JSON (`{ token, url, port, pid }`) to a per-session file `<project>/.arkor/agent/session-<pid>-<uuid>.json` (agent dir 0700, file 0600, atomic temp+rename; the parent `.arkor` is created with the default mode, NOT 0700, so a first-run agent launch does not tighten a directory `arkor build`/state.json share) and prints the path to stdout as the stable line `Arkor Studio agent session file: <path>` (the e2e/studio harness and coding agents grep it; don't change the prefix). Unlike the home token this write is FAIL-HARD (it is the agent's only token channel). The exact session path is computed BEFORE the write and captured, so the shutdown handlers unlink that one path (plus its deterministic `.tmp` sibling) without a readdir sweep: matching a bare `session-<pid>-*` prefix would delete a co-located live session that happens to share this pid (real under two containers bind-mounting the same project, both running as pid 1). Agent mode does NOT sweep or reap other session files (an earlier pid-liveness reap was removed: `process.kill(pid, 0)` is namespace-local, so on a shared bind-mount a LIVE foreign-container session looks dead and would be deleted). A crashed session leaves an inert file; the docs recipe selects the newest (`ls -t`), which in the single-session case is always the live launch (leftovers from sessions that crashed BEFORE it started are older; a session launched after it that crashed can leave a newer file), and with overlapping `--agent` launches in one project only the stdout-printed path is unambiguous. Under `CLAUDECODE=1`, plain `arkor dev` exits 1 via the `ClaudeCodeStrictExit` sentinel asking for `--agent`; the flag itself needs no env var. The session file `url` (and the `/api/status` echo) is the agent-facing `http://127.0.0.1:<port>` literal, NOT `localhost` (an agent's HTTP client may not do Happy-Eyeballs, so it must get the IPv4 the server actually bound); the human-facing stdout line and the `--open` browser target still use `localhost`. `GET /api/status` is the token-EXEMPT, secrets-free probe (`server: "arkor-studio"` discriminator, echoes `cwd`, never executes user code); normal-mode `arkor dev` on a busy port probes `http://127.0.0.1:<port>/api/status` WITHOUT a token, and adopts the occupant only when it is an Arkor Studio AND its `cwd` (realpath, must be absolute) equals this launch's project root -> prints `Arkor Studio already running on <url>` and exits 0 instead of EADDRINUSE-failing. A different project on the port, or a non-Studio occupant, falls back to the hard port-in-use error. Adoption is UNAUTHENTICATED (it trusts the loopback peer's self-reported `/api/status`); this is deliberate because the probe sends no token, so an impostor can never obtain the CSRF token or reach `POST /api/train` (RCE) -- worst case is a nuisance denial/`--open` redirect, never code execution. Agent mode never adopts an existing instance (needs its own server + session file); it hard-errors and advises `--port`. Both e2e harnesses strip `CLAUDECODE` from spawned-child envs so the gate can't leak into tests from a Claude Code session.

`/api/*` middleware also enforces a host-header allow-list (`127.0.0.1`/`localhost`) for DNS-rebinding defence. **CORS is intentionally NOT configured**: the SPA is same-origin so reflecting `*` would let "simple" cross-origin POSTs reach handlers. The token check rejects those; cross-origin tabs cannot read the SPA's `<meta>`.

The whole point: prevents another browser tab on the same machine from POSTing `/api/train` (which spawns `arkor train` and dynamically imports user TS: RCE-grade).
The whole point: prevents another browser tab on the same machine from POSTing `/api/train` (which spawns `arkor start` and dynamically imports user TS: RCE-grade). Note the route is `/api/train` but the child it spawns is `arkor start`; there is no `arkor train` command.

When touching the Studio server or SPA fetch layer, preserve: token via header for `fetch`, query param for `EventSource`, host-header guard, no CORS, timing-safe compare. The Vite plugin is dev-only (`apply: "serve"`): running it during `vite build` would bake a stale per-launch token into the production `index.html` and shadow the runtime tag, causing every `/api/*` call to 403.

Expand Down Expand Up @@ -115,7 +116,7 @@ Don't split these into "docs in a follow-up PR" or "tests later"; land them in t

Cross-page links in `docs/` must therefore mirror the actual rendered id, which preserves `/`, `=`, and full-width parens but strips ASCII parens and backticks. Always confirm before adding or editing an anchor link: `curl -s "<mintlify-preview-url>/<page>" | grep -oE 'id="[^"]*<keyword>[^"]*"'`. Do not act on review comments (human or bot) claiming "Mintlify strips punctuation in headings" as a universal rule: Copilot in particular gets this wrong repeatedly by extrapolating from `github-slugger`, and has even cited the pre-existing broken `#ci--non-interactive-shells` link as supposed evidence. PR #141 fixed that broken link to `#ci-/-non-interactive-shells`, and the surviving anchors in `docs/cli/overview.mdx` and the strict-mode sections of `docs/quickstart.mdx` / `docs/cli/init.mdx` (plus their JA mirrors) are the canonical examples to copy from.
- **Don't call a HuggingFace model name "non-existent"** based on training-data alone. Templates reference real models (e.g. `unsloth/gemma-4-E4B-it`) that may post-date Claude's knowledge cutoff. Verify (e.g. `WebFetch`) before flagging in issues or PR comments. If unverifiable, hedge ("could not confirm") rather than asserting absence.
- **Generated files** copied into package dirs are gitignored: `packages/*/CONTRIBUTING.md` (from root), `packages/arkor/docs/` (from root `docs/`). Edit the source under repo root, not the copies.
- **Generated files** copied into package dirs are gitignored: `packages/*/CONTRIBUTING.md` and `packages/*/README.ja.md` (both from root, via `packages/arkor/scripts/copy-root-files.mjs`), `packages/arkor/docs/` (from root `docs/`). Edit the source under repo root, not the copies. Note `packages/arkor/README.md` is NOT generated: it is its own tracked file (the published npm page), so an English change there has no root counterpart to mirror.
- **Node version**: published packages declare `engines.node >=22.22.0` (raised from 22.6 to dodge the [Jan 2026 async-hooks DoS CVE](https://nodejs.org/en/blog/vulnerability/january-2026-dos-mitigation-async-hooks)). Use Node 24 (latest preferred) for development per [CONTRIBUTING.md](CONTRIBUTING.md).
- **pnpm policy** ([pnpm-workspace.yaml](pnpm-workspace.yaml)): `minimumReleaseAge: 1440` (24 h), `trustPolicy: no-downgrade`, and `blockExoticSubdeps: true` are intentional supply-chain guards. `minimumReleaseAgeExclude` carries a single narrow carve-out for our own `@arkor/cloud-api-client` so this submodule can pick up alpha publishes the same hour they ship; the trade-off is documented inline in `pnpm-workspace.yaml` and the entry should not grow without re-evaluating it. `blockExoticSubdeps` blocks transitive deps from resolving through `git+ssh://` or direct tarball URLs that would bypass the release-age and downgrade gates; we keep it set explicitly as policy (not as a transitional pin for pnpm 10.x's opt-in default) so the supply-chain stance stays co-located with the other guards and survives any future default change upstream. `allowBuilds` is the explicit per-package map for postinstall scripts: `rolldown`, `unrs-resolver`, and `esbuild` are approved (`true`) because the SDK build path needs them; the Mintlify-side transitives `keytar`, `puppeteer`, and `sharp` are explicitly denied (`false`) so pnpm 11's `ERR_PNPM_IGNORED_BUILDS` stops failing the install without granting docs-only postinstalls the right to run install-time code. Flip a denied entry to `true` only if a concrete docs task starts failing because its native binary is missing.
- **Dependency catalog** ([pnpm-workspace.yaml](pnpm-workspace.yaml) `catalog:` block): direct third-party (registry) deps declared in workspace `package.json` files resolve through the default catalog instead of carrying literal semver per `package.json`; each consumer references the entry as `"<name>": "catalog:"`. The point is one source of truth: a version bump is a single-line edit in `pnpm-workspace.yaml`, not N parallel edits across `packages/*` and `e2e/*` that risk drifting out of sync. The catalog only covers **direct** deps; transitive deps fall back to the lockfile and the supply-chain guards above. To bump a dep, edit the catalog entry and run `pnpm install` (do NOT `pnpm add -w <pkg>@<v>` against a catalog entry: it would rewrite the workspace's `package.json` from `catalog:` to a literal version and re-introduce the drift). Workspace-internal links (`@arkor/cli-internal`, `arkor`, `create-arkor`) stay as `workspace:*` since catalogs only cover registry versions. **Carve-out**: the **runtime `dependencies`** of the two published packages ([`packages/arkor/package.json`](packages/arkor/package.json) and [`packages/create-arkor/package.json`](packages/create-arkor/package.json)) keep literal semver. This carve-out predates the switch to `pnpm publish`: the [release workflow](.github/workflows/build.yaml) used to publish via `npm publish` (run from the package directory so the npm CLI embedded `README.md` into the registry packument), and `npm publish` does not rewrite the pnpm-specific `catalog:` protocol, so consumers would have seen `EUNSUPPORTEDPROTOCOL`. The workflow now publishes via `pnpm publish` with `embedReadme: true` set in [pnpm-workspace.yaml](pnpm-workspace.yaml) (pnpm 11's native publish does not embed the README by default), and `pnpm publish` *does* resolve `catalog:`/`workspace:` to concrete versions in the published manifest, so the `EUNSUPPORTEDPROTOCOL` hazard is gone. The runtime deps are still kept literal on purpose, so the published manifest mirrors source exactly with no publish-time catalog-expansion step to reason about. Their `devDependencies` are catalog-bound as normal: `pnpm pack` does still ship the field in the published `package.json`, but `npm install <pkg>` on the consumer side never installs a dependency's `devDependencies`, so the `catalog:` protocol there is never resolved at install time and cannot trigger `EUNSUPPORTEDPROTOCOL`.
Expand Down
4 changes: 2 additions & 2 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pnpm dev
```

**サインアップ不要:**
`arkor dev` は **Studio** と呼ばれるローカル Web UI を `http://localhost:4000` で開きます。初回起動時に使い捨ての匿名ワークスペースをプロビジョニングするので、すぐに実際のトレーニング実行を開始できます。
`arkor dev` は **Studio** と呼ばれるローカル Web UI を `http://localhost:4000` で開きます。初回起動時に使い捨ての匿名ワークスペースをプロビジョニングするので、すぐに実際のトレーニング実行を開始できます。コーディングエージェントは `arkor dev --agent` を使ってください。同じ Studio をヘッドレスでサーブし、API トークンを `.arkor/agent/` 配下の JSON セッションファイルに書き出します。`CLAUDECODE=1` の下では素の `arkor dev` は起動を拒否し、このフラグを要求します。

後からアカウントに紐付けたい場合は `arkor login --oauth` を実行してください。

Expand Down Expand Up @@ -152,7 +152,7 @@ my-arkor-app/

`arkor dev` は [Hono](https://hono.dev) サーバーを `127.0.0.1:4000` で起動し、同一オリジンから Vite + React SPA を配信します。

SPA は起動ごとの CSRF トークンでゲートされた `/api/*` ルート (ループバック専用、DNS リバインディング対策の `Host` ヘッダーガード付き) を通じてあなたのコードと通信します。あなたのコードは認証付き HTTPS で Arkor トレーニングバックエンドと通信します。
Studio サーバーは HTML を配信する前の段階で、ループバック以外の `Host` ヘッダーを拒否します (DNS リバインディング対策)。SPA は起動ごとの CSRF トークンでゲートされた `/api/*` ルートを通じてあなたのコードと通信します。あなたのコードは認証付き HTTPS で Arkor トレーニングバックエンドと通信します。

トレーニングはマネージド GPU 上で実行され、チェックポイントは SSE イベントとしてストリームバックされ、プロセス内であなたの `callbacks.*` を発火させます。

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pnpm dev
```

**No signup required:**
`arkor dev` opens **Studio**, a local web UI at `http://localhost:4000`. On first launch it provisions a throwaway anonymous workspace so you can fire off a real training run right away.
`arkor dev` opens **Studio**, a local web UI at `http://localhost:4000`. On first launch it provisions a throwaway anonymous workspace so you can fire off a real training run right away. Coding agents should use `arkor dev --agent`, which serves the same Studio headlessly and writes an API token to a JSON session file under `.arkor/agent/`; under `CLAUDECODE=1` a plain `arkor dev` refuses to start and asks for the flag.

Run `arkor login --oauth` later if you want to claim your work under an account.

Expand Down Expand Up @@ -145,7 +145,7 @@ my-arkor-app/

`arkor dev` boots a [Hono](https://hono.dev) server on `127.0.0.1:4000` that serves a Vite + React SPA from the same origin.

The Studio server rejects non-loopback `Host` headers before serving HTML, and the SPA talks to your code via per-launch CSRF-token-gated `/api/*` routes; your code talks to the Arkor training backend over authenticated HTTPS.
The Studio server rejects non-loopback `Host` headers before serving HTML (DNS-rebinding defence), and the SPA talks to your code via per-launch CSRF-token-gated `/api/*` routes; your code talks to the Arkor training backend over authenticated HTTPS.

Training runs on managed GPUs; checkpoints stream back as SSE events that fire your `callbacks.*` in process.

Expand Down
4 changes: 2 additions & 2 deletions docs/cli/build-and-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ The "reuse the artifact" path is what lets Studio surface trainer edits via its
| Message | What it means | Fix |
| --- | --- | --- |
| `Build entry not found: <abs-path>. Create src/arkor/index.ts or pass an explicit entry argument.` | `arkor start` runs `arkor build` first whenever you pass an entry argument, or whenever `.arkor/build/index.mjs` is missing. A bad entry path surfaces here, not at the runner stage. | Pass an entry that exists, or omit it and rely on the default `src/arkor/index.ts`. |
| `Training entry must export 'arkor' (from createArkor({...})) or 'trainer' (from createTrainer({...})), or default-export one of them.` | The bundle imported successfully but did not expose any of the supported export shapes. | See [Project structure § `src/arkor/`](/concepts/project-structure#srcarkor) for the three accepted forms (named `arkor`, named `trainer`, or default). |
| `Training entry must export 'arkor' (from createArkor({...})) or 'trainer' (from createTrainer({...})), or default-export one of them.` | The bundle imported successfully but did not expose any of the supported export shapes. | See [Project structure § `src/arkor/`](/concepts/project-structure#src/arkor/) for the three accepted forms (named `arkor`, named `trainer`, or default). |

`runTrainer` (programmatic):

Expand Down Expand Up @@ -198,6 +198,6 @@ bun run build && bun start

## See also

- [Project structure § `src/arkor/`](/concepts/project-structure#srcarkor) for the export shapes the runner accepts
- [Project structure § `src/arkor/`](/concepts/project-structure#src/arkor/) for the export shapes the runner accepts
- [`runTrainer`](/sdk/overview#auxiliary-helpers-advanced) for driving the same runtime path from your own TypeScript code
- [Programmatic runs recipe](/cookbook/programmatic-runs) for a full server / script wiring
Loading
Loading