@usine/gateway 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -2
  3. package/dist/admin/api.d.ts +206 -0
  4. package/dist/admin/api.d.ts.map +1 -0
  5. package/dist/admin/api.js +105 -0
  6. package/dist/admin/api.js.map +1 -0
  7. package/dist/admin/server.d.ts +10 -0
  8. package/dist/admin/server.d.ts.map +1 -0
  9. package/dist/admin/server.js +79 -0
  10. package/dist/admin/server.js.map +1 -0
  11. package/dist/attachment/attachment.d.ts +77 -0
  12. package/dist/attachment/attachment.d.ts.map +1 -0
  13. package/dist/attachment/attachment.js +57 -0
  14. package/dist/attachment/attachment.js.map +1 -0
  15. package/dist/attachment/store.d.ts +27 -0
  16. package/dist/attachment/store.d.ts.map +1 -0
  17. package/dist/attachment/store.js +101 -0
  18. package/dist/attachment/store.js.map +1 -0
  19. package/dist/caller/caller.d.ts +60 -0
  20. package/dist/caller/caller.d.ts.map +1 -0
  21. package/dist/caller/caller.js +130 -0
  22. package/dist/caller/caller.js.map +1 -0
  23. package/dist/egress/client-hello.d.ts +120 -0
  24. package/dist/egress/client-hello.d.ts.map +1 -0
  25. package/dist/egress/client-hello.js +155 -0
  26. package/dist/egress/client-hello.js.map +1 -0
  27. package/dist/egress/egress.d.ts +66 -0
  28. package/dist/egress/egress.d.ts.map +1 -0
  29. package/dist/egress/egress.js +114 -0
  30. package/dist/egress/egress.js.map +1 -0
  31. package/dist/gateway.d.ts +23 -0
  32. package/dist/gateway.d.ts.map +1 -0
  33. package/dist/gateway.js +23 -0
  34. package/dist/gateway.js.map +1 -0
  35. package/dist/github/github.d.ts +43 -0
  36. package/dist/github/github.d.ts.map +1 -0
  37. package/dist/github/github.js +88 -0
  38. package/dist/github/github.js.map +1 -0
  39. package/dist/llm/llm.d.ts +64 -0
  40. package/dist/llm/llm.d.ts.map +1 -0
  41. package/dist/llm/llm.js +92 -0
  42. package/dist/llm/llm.js.map +1 -0
  43. package/dist/node/egress-proxy.d.ts +30 -0
  44. package/dist/node/egress-proxy.d.ts.map +1 -0
  45. package/dist/node/egress-proxy.js +491 -0
  46. package/dist/node/egress-proxy.js.map +1 -0
  47. package/dist/node/environment.d.ts +108 -0
  48. package/dist/node/environment.d.ts.map +1 -0
  49. package/dist/node/environment.js +113 -0
  50. package/dist/node/environment.js.map +1 -0
  51. package/dist/node/main.d.ts +3 -0
  52. package/dist/node/main.d.ts.map +1 -0
  53. package/dist/node/main.js +13 -0
  54. package/dist/node/main.js.map +1 -0
  55. package/dist/node/node.d.ts +29 -0
  56. package/dist/node/node.d.ts.map +1 -0
  57. package/dist/node/node.js +82 -0
  58. package/dist/node/node.js.map +1 -0
  59. package/dist/node/reload.d.ts +26 -0
  60. package/dist/node/reload.d.ts.map +1 -0
  61. package/dist/node/reload.js +142 -0
  62. package/dist/node/reload.js.map +1 -0
  63. package/dist/proxy/forward.d.ts +12 -0
  64. package/dist/proxy/forward.d.ts.map +1 -0
  65. package/dist/proxy/forward.js +44 -0
  66. package/dist/proxy/forward.js.map +1 -0
  67. package/dist/proxy/proxy.d.ts +28 -0
  68. package/dist/proxy/proxy.d.ts.map +1 -0
  69. package/dist/proxy/proxy.js +189 -0
  70. package/dist/proxy/proxy.js.map +1 -0
  71. package/dist/settings.d.ts +52 -0
  72. package/dist/settings.d.ts.map +1 -0
  73. package/dist/settings.js +27 -0
  74. package/dist/settings.js.map +1 -0
  75. package/dist/upstream.d.ts +5 -0
  76. package/dist/upstream.d.ts.map +1 -0
  77. package/dist/upstream.js +6 -0
  78. package/dist/upstream.js.map +1 -0
  79. package/package.json +58 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Philippe L'ATTENTION
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,113 @@
1
- # Temporary Holding Version
1
+ # @usine/gateway
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The service on the gateway host in front of usine's worker VMs ([ADR 0027](../../docs/adr/0027-gateway-credential-proxy.md), [#223](https://github.com/basaltbytes/usine/issues/223)). Worker VMs have no public address, GitHub token, or API key. They reach GitHub through the gateway, which adds the credential. They reach an LLM provider the same way if the gateway has that provider's API key, and otherwise with the harness's own login through the gateway's forward proxy, which allows only allowlisted hosts: package registries and the harness login hosts. [Workers and the gateway](https://docs.usine.dev/architecture/workers-and-gateway/) describes the deployment.
4
+
5
+ The gateway serves:
6
+
7
+ - **A reverse proxy** for workers on the private network. `git.<domain>` serves git smart HTTP at `/OWNER/REPO.git` and the GitHub REST and GraphQL API under `/api/`, as GitHub Enterprise Server does, so `gh` works with `GH_HOST=git.<domain>`. It adds the installation token that the executor attached for that worker and repo. `llm.<domain>` serves the Anthropic API under `/anthropic` and an OpenAI-compatible API under `/openai` with the gateway's API key. Other hosts are refused; the forward proxy serves the allowlisted ones.
8
+ - **A forward proxy** for `HTTPS_PROXY` and `HTTP_PROXY`: `CONNECT` on port 443 for HTTPS and absolute-form requests for plain HTTP mirrors, to allowlisted hosts only. A tunnel opens only if the client's TLS ClientHello names the CONNECT host. A ClientHello naming another host or none, an encrypted ClientHello, or non-TLS bytes end the tunnel (`server-name-mismatch`, `server-name-missing`, `not-tls`). It resolves each name itself and refuses private, loopback, and link-local addresses. Before a plain HTTP request leaves, it removes `X-Usine-Gateway-Token` and any `Authorization` or `x-api-key` value that is a caller token, and leaves other credentials alone.
9
+ - **An admin API** for the executor: attach, renew, detach, and list a worker's repo credentials, and list the [LLM providers](#llm-providers) it has credentials for. It requires the admin token as a bearer token. Attachments are kept in memory; the executor attaches again when it renews or restarts. A [reload](#reload) keeps them.
10
+
11
+ Refused requests log `Gateway refused a request` at warning level, with the reason, host, client address, and the caller token's worker. Forwarded requests log at info with the worker, repo, method, path, and status. No token, key, or request body is logged.
12
+
13
+ ## Install
14
+
15
+ `@usine/gateway` is on npm from 0.1.0. On the gateway host, this installs it with its `usine-gateway` command in `node_modules/.bin`:
16
+
17
+ ```sh
18
+ npm install --omit=dev @usine/gateway
19
+ ```
20
+
21
+ To run an unreleased build, pack it in a usine checkout with the `@usine/core` it depends on, whose published version can lag, and install both tarballs with one `npm install`:
22
+
23
+ ```sh
24
+ pnpm build
25
+ pnpm --filter @usine/core --filter @usine/gateway pack --pack-destination .
26
+ npm install --omit=dev ./usine-core-<version>.tgz ./usine-gateway-<version>.tgz
27
+ ```
28
+
29
+ On Hetzner Cloud, `gateway.sh install` does either on the gateway VM and restarts the service; see [`infra/hetzner/gateway/README.md`](../../infra/hetzner/gateway/README.md).
30
+
31
+ ## Entry points
32
+
33
+ | Import | Contents |
34
+ | --- | --- |
35
+ | `@usine/gateway` | The platform-free core: `GatewaySettings`, `SettingsInForce` (the allowlist and LLM providers that a reload replaces), `Attachments`, `CallerTokens`, `GatewayProxy` (the reverse proxy as an `HttpServerRequest` handler), `adminRoutes` (the admin API for an `HttpRouter`), `gatewayServices`, the egress allowlist parser, and the domain records. It needs an `HttpClient` that leaves `content-encoding` alone. |
36
+ | `@usine/gateway/admin` | `GatewayAdminApi`, the admin API contract for `HttpApiClient.make` in the executor, with the records a client sends and reads (`AttachRequest`, `InstallationCredential`, `Attached`, `AttachmentId`, `CallerToken`, `Detached`, `AttachmentList`, `LlmProviderList`) and `GATEWAY_TOKEN_HEADER`. |
37
+ | `@usine/gateway/node` | `gatewayLayer` (the three servers on Node, with undici upstream), `gatewayFromEnvironment`, `settingsFromEnvironment`, `GatewayAddresses`, and `GatewayReload`. |
38
+ | `usine-gateway` | The gateway VM's command. It reads the settings below, reloads on SIGHUP, and logs one JSON object per line. |
39
+
40
+ `GatewayDomain` is re-exported from `@usine/core/worker`; gateway settings and worker configuration share its lowercase DNS domain schema.
41
+
42
+ ## Attachments and caller tokens
43
+
44
+ At the start of an attempt, the executor attaches the attempt's installation token for the worker and repo, with its expiry:
45
+
46
+ ```http
47
+ POST /v1/attachments
48
+ Authorization: Bearer <admin token>
49
+
50
+ {"worker": "worker-one", "repository": "acme/demo",
51
+ "credential": {"_tag": "InstallationCredential", "token": "ghs_..."},
52
+ "expiresAt": "2026-10-13T18:00:00Z"}
53
+ ```
54
+
55
+ The response has the attachment without the token, including its assigned `id`, and a caller token, which the executor gives the attempt's children in place of every credential. The caller token names the attachment id, worker, and repo, and is signed with the gateway's caller key. Each attempt gets its own attachment, even on a shared worker and repo.
56
+
57
+ Before the installation token expires, the executor renews with `PUT /v1/attachments/<id>` and the same body with the new token. The response has the same caller token, so children keep working, and after a gateway restart the renewal stores the attachment again under its id. `DELETE /v1/attachments/<id>` detaches; `GET /v1/attachments` lists attachments in force.
58
+
59
+ A caller token works only while its attachment is in force. After a detach or token expiry, every request with it is refused, LLM requests included, and a new attach for the same worker and repo gets a new id, so the old token stays refused (`stale-caller-token`). A request whose path names another repo than the token is refused. An `AnonymousCredential` attachment adds no credential, which is enough to read a public repo.
60
+
61
+ A child sends its caller token wherever its tool sends a credential: the `X-Usine-Gateway-Token` header, `Authorization` as `Bearer`, `token`, or `Basic` (password), or `x-api-key`. The gateway removes all of them before forwarding.
62
+
63
+ ## LLM providers
64
+
65
+ `GET /v1/providers` lists the providers the gateway has a credential for, with each credential's kind, never the credential:
66
+
67
+ ```json
68
+ {"providers": [{"provider": "anthropic", "credential": "api-key"}]}
69
+ ```
70
+
71
+ `credential` is `api-key` for `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`, and `subscription` for `USINE_GATEWAY_ANTHROPIC_SUBSCRIPTION_TOKEN`. Providers without a credential are absent. The list follows a [reload](#reload). The executor reads it when it checks a worker whose `llm_providers` lists `anthropic`, so usage counts Claude Code children on the gateway's API key in the API keys pool, not the Claude plan ([ADR 0013](../../docs/adr/0013-cost-accounting-native-meters-guardrails.md)). Gateways released before this endpoint answer 404.
72
+
73
+ ## Worker environment
74
+
75
+ `infra/hetzner/gateway/worker-cloud-init.yaml` sets up the machine. For each attempt, the executor adds the caller token and sends each variable that contains it over the SSH connection, not the command line; the worker's sshd accepts them with `AcceptEnv`. The Claude Code row applies if the gateway has the Anthropic credential; otherwise Claude Code, like Codex, uses its login on the worker and `HTTPS_PROXY`. The Codex row is what a Codex child would need to use the gateway's OpenAI route; the executor doesn't set it ([#235](https://github.com/basaltbytes/usine/issues/235)).
76
+
77
+ | Tool | Setting |
78
+ | --- | --- |
79
+ | git | remote `http://git.<domain>/OWNER/REPO.git`; `GIT_CONFIG_KEY_0=http.http://git.<domain>/.extraHeader`, `GIT_CONFIG_VALUE_0="X-Usine-Gateway-Token: <caller token>"`, and `url.http://git.<domain>/.insteadOf` for `https://github.com/` and `https://git.<domain>/` |
80
+ | gh | `GH_HOST=git.<domain>`, `GH_ENTERPRISE_TOKEN=<caller token>`, `GH_TOKEN=<caller token>`; gh's config sets `http_unix_socket: /run/usine-gateway/http.sock`, which `systemd-socket-proxyd` forwards to the gateway, because gh speaks only HTTPS to a custom host but plain HTTP over a socket |
81
+ | Claude Code | `ANTHROPIC_BASE_URL=http://llm.<domain>/anthropic`, `ANTHROPIC_AUTH_TOKEN=<caller token>` |
82
+ | Codex | a `model_provider` with `base_url = "http://llm.<domain>/openai/v1"`, `env_key` naming a variable that contains the caller token, `wire_api = "responses"` |
83
+ | package managers, harness logins | `HTTPS_PROXY` and `HTTP_PROXY` set to `http://<gateway>:3128`, `NO_PROXY=<domain>` |
84
+
85
+ A client that sends every request through `HTTPS_PROXY`, as Claude Code does, still reaches `git.<domain>` and `llm.<domain>`: the forward proxy sends their port 80 requests to the reverse proxy.
86
+
87
+ ## Configuration
88
+
89
+ The [CLI reference](https://docs.usine.dev/reference/configuration/cli/) lists every `usine-gateway` variable, with defaults and the environment file rules. Changing `USINE_GATEWAY_CALLER_KEY` invalidates every caller token.
90
+
91
+ The gateway sends `ANTHROPIC_API_KEY` as `x-api-key` on `/anthropic`, `USINE_GATEWAY_ANTHROPIC_SUBSCRIPTION_TOKEN` (from `claude setup-token`) as a bearer token with the OAuth beta flag, and `OPENAI_API_KEY` as a bearer token on `/openai`. `USINE_GATEWAY_OPENAI_UPSTREAM` can be any OpenAI-compatible provider. Run the placeholder swap test in [Tests](#tests) before you rely on the subscription token. A provider without a credential is refused with `provider-not-configured`.
92
+
93
+ ## Reload
94
+
95
+ On SIGHUP, `usine-gateway` rereads its environment files and allowlist, and replaces the egress allowlist and the LLM provider settings: `ANTHROPIC_API_KEY`, `USINE_GATEWAY_ANTHROPIC_SUBSCRIPTION_TOKEN`, `OPENAI_API_KEY`, both upstreams, and `USINE_GATEWAY_EGRESS_ALLOWLIST`. Under systemd, `ExecReload=kill -HUP $MAINPID` makes `systemctl reload usine-gateway` send it.
96
+
97
+ Attachments, the admin API, listening sockets, and open tunnels stay, so running attempts keep their GitHub and LLM requests. An open tunnel stays until a side closes it; each new `CONNECT` or plain HTTP request uses the new allowlist, and each later LLM request the new key. The domain, listeners, admin token, and caller key change only with a restart. A reload that finds one of them changed logs `Settings changed that take effect only after a restart` at warning level with the variable names, and applies the rest.
98
+
99
+ Each reload ends with `Reloaded the gateway settings`, which gives the environment files, the allowlist path and rule count, whether the allowlist and providers were reloaded or kept, and the names, never values, of changed provider settings (`changed`). Unusable input keeps what's in force:
100
+
101
+ | Problem | The reload keeps | Log line, at error level |
102
+ | --- | --- | --- |
103
+ | The allowlist can't be read or doesn't parse | The previous allowlist | `Kept the previous egress allowlist`, with the path, the line, and the problem |
104
+ | A provider setting is invalid, or both Anthropic credentials are set | The previous providers | `Kept the previous LLM providers` |
105
+ | An environment file can't be read | Everything | `Kept the gateway settings: an environment file could not be read`, with the path |
106
+
107
+ `systemctl reload` returns before the reload ends; the journal has the result. Variables from the process environment at startup don't change, so a gateway without environment files reloads only its allowlist.
108
+
109
+ ## Tests
110
+
111
+ `pnpm --filter @usine/gateway test` runs the unit tests and an end-to-end test that starts the gateway on loopback ports, clones `octocat/Hello-World` through it, calls `gh api` through a Unix socket as a worker does, and opens a tunnel to `registry.npmjs.org`. It needs network access but no secret.
112
+
113
+ `pnpm --filter @usine/gateway test:placeholder-swap` checks whether Claude Code works when the worker has only a caller token as `CLAUDE_CODE_OAUTH_TOKEN` and the gateway sends a subscription token instead. It needs `claude` on `PATH`. The first case runs Claude Code against a local stand-in for the Anthropic API and checks what reached it. The second runs against `api.anthropic.com` with the real token in `CLAUDE_CODE_OAUTH_TOKEN`, and is skipped without it.
@@ -0,0 +1,206 @@
1
+ import { Schema } from "effect";
2
+ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi";
3
+ import { type Attachment, AttachmentAlreadyExpired, AttachmentConflict } from "../attachment/attachment.ts";
4
+ import { type LlmProviders } from "../llm/llm.ts";
5
+ export { AttachmentId, InstallationCredential } from "../attachment/attachment.ts";
6
+ export { CallerToken, GATEWAY_TOKEN_HEADER } from "../caller/caller.ts";
7
+ declare const AttachRequest_base: Schema.Class<AttachRequest, Schema.Struct<{
8
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
9
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
10
+ readonly credential: Schema.Union<readonly [typeof import("../attachment/attachment.ts").InstallationCredential, typeof import("../attachment/attachment.ts").AnonymousCredential]>;
11
+ readonly expiresAt: Schema.DateTimeUtc;
12
+ }>, {}>;
13
+ /** A request to attach one Attempt's credential for one Worker and repository until it expires. */
14
+ export declare class AttachRequest extends AttachRequest_base {
15
+ }
16
+ declare const AttachmentSummary_base: Schema.Class<AttachmentSummary, Schema.Struct<{
17
+ readonly id: Schema.brand<Schema.String, "AttachmentId">;
18
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
19
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
20
+ readonly credential: Schema.Literals<readonly ["InstallationCredential", "AnonymousCredential"]>;
21
+ readonly expiresAt: Schema.DateTimeUtc;
22
+ }>, {}>;
23
+ /** An attachment without its token. */
24
+ export declare class AttachmentSummary extends AttachmentSummary_base {
25
+ static fromAttachment(attachment: Attachment): AttachmentSummary;
26
+ }
27
+ declare const Attached_base: Schema.Class<Attached, Schema.Struct<{
28
+ readonly attachment: typeof AttachmentSummary;
29
+ readonly callerToken: Schema.Redacted<Schema.NonEmptyString>;
30
+ }>, {}>;
31
+ /**
32
+ * The response to an attach or renew: the attachment and the Worker VM's caller token.
33
+ * Renewing the same attachment ID returns the same token.
34
+ */
35
+ export declare class Attached extends Attached_base {
36
+ }
37
+ declare const Detached_base: Schema.Class<Detached, Schema.Struct<{
38
+ readonly detached: Schema.Boolean;
39
+ }>, {}>;
40
+ /** The response to a detach. `detached` is whether the attachment existed. */
41
+ export declare class Detached extends Detached_base {
42
+ }
43
+ declare const AttachmentList_base: Schema.Class<AttachmentList, Schema.Struct<{
44
+ readonly attachments: Schema.$Array<typeof AttachmentSummary>;
45
+ }>, {}>;
46
+ export declare class AttachmentList extends AttachmentList_base {
47
+ }
48
+ declare const LlmProviderSummary_base: Schema.Class<LlmProviderSummary, Schema.Struct<{
49
+ readonly provider: Schema.Literals<readonly ["anthropic", "openai"]>;
50
+ readonly credential: Schema.Literals<readonly ["api-key", "subscription"]>;
51
+ }>, {}>;
52
+ /** An LLM provider the Gateway has a credential for, with the credential kind but not the credential. */
53
+ export declare class LlmProviderSummary extends LlmProviderSummary_base {
54
+ }
55
+ declare const LlmProviderList_base: Schema.Class<LlmProviderList, Schema.Struct<{
56
+ readonly providers: Schema.$Array<typeof LlmProviderSummary>;
57
+ }>, {}>;
58
+ /** The LLM providers the Gateway forwards to, with each credential kind. Providers without a credential aren't listed. */
59
+ export declare class LlmProviderList extends LlmProviderList_base {
60
+ static fromProviders({ anthropic, openai }: LlmProviders): LlmProviderList;
61
+ }
62
+ declare const AdminAuthorization_base: HttpApiMiddleware.ServiceClass<AdminAuthorization, "@usine/gateway/AdminAuthorization", {
63
+ requires: never;
64
+ provides: never;
65
+ error: typeof HttpApiError.Unauthorized;
66
+ clientError: never;
67
+ requiredForClient: false;
68
+ security: {
69
+ readonly bearer: HttpApiSecurity.Http;
70
+ };
71
+ }, HttpApiMiddleware.HttpApiMiddlewareSecurity<{
72
+ readonly bearer: HttpApiSecurity.Http;
73
+ }, never, typeof HttpApiError.Unauthorized, never>>;
74
+ /**
75
+ * Middleware that requires the admin token as a bearer token on every admin request, and
76
+ * responds with 401 otherwise. Clients add the header themselves, for example with
77
+ * `HttpClientRequest.bearerToken`.
78
+ */
79
+ export declare class AdminAuthorization extends AdminAuthorization_base {
80
+ }
81
+ /**
82
+ * The admin API the executor uses to attach, renew, and detach a Worker VM's GitHub credential
83
+ * for an Attempt, and to read the Gateway's LLM provider credential kinds (ADR 0013).
84
+ * Attachments are in memory; after a Gateway restart, the executor's next renewal re-creates
85
+ * each one under the same ID, so caller tokens stay valid.
86
+ */
87
+ export declare const GatewayAdminApi: HttpApi.HttpApi<"GatewayAdminApi", HttpApiGroup.HttpApiGroup<"attachments", HttpApiEndpoint.HttpApiEndpoint<"detach", "DELETE", "/v1/attachments/:id", Schema.toCodecStringTree<Schema.Struct<{
88
+ id: Schema.brand<Schema.String, "AttachmentId">;
89
+ }>>, never, never, never, Schema.toCodecJson<typeof Detached>, never, AdminAuthorization, never> | HttpApiEndpoint.HttpApiEndpoint<"list", "GET", "/v1/attachments", never, never, never, never, Schema.toCodecJson<typeof AttachmentList>, never, AdminAuthorization, never> | HttpApiEndpoint.HttpApiEndpoint<"attach", "POST", "/v1/attachments", never, never, Schema.toCodecJson<Schema.decodeTo<Schema.declareConstructor<AttachRequest, {
90
+ readonly worker: string;
91
+ readonly repository: string;
92
+ readonly credential: {
93
+ readonly _tag: "InstallationCredential";
94
+ readonly token: import("effect/Redacted").Redacted<string>;
95
+ } | {
96
+ readonly _tag: "AnonymousCredential";
97
+ };
98
+ readonly expiresAt: import("effect/DateTime").Utc;
99
+ }, readonly [Schema.Struct<{
100
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
101
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
102
+ readonly credential: Schema.Union<readonly [typeof import("../attachment/attachment.ts").InstallationCredential, typeof import("../attachment/attachment.ts").AnonymousCredential]>;
103
+ readonly expiresAt: Schema.DateTimeUtc;
104
+ }>], {
105
+ readonly worker: string & import("effect/Brand").Brand<"WorkerName">;
106
+ readonly repository: string & import("effect/Brand").Brand<"RepositoryName">;
107
+ readonly credential: {
108
+ readonly _tag: "InstallationCredential";
109
+ readonly token: import("effect/Redacted").Redacted<string>;
110
+ } | {
111
+ readonly _tag: "AnonymousCredential";
112
+ };
113
+ readonly expiresAt: import("effect/DateTime").Utc;
114
+ }>, Schema.Struct<{
115
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
116
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
117
+ readonly credential: Schema.Union<readonly [typeof import("../attachment/attachment.ts").InstallationCredential, typeof import("../attachment/attachment.ts").AnonymousCredential]>;
118
+ readonly expiresAt: Schema.DateTimeUtc;
119
+ }>, never, never>>, never, Schema.toCodecJson<typeof Attached>, Schema.toCodecJson<Schema.decodeTo<Schema.declareConstructor<AttachmentAlreadyExpired, {
120
+ readonly _tag: "AttachmentAlreadyExpired";
121
+ readonly worker: string;
122
+ readonly repository: string;
123
+ readonly expiresAt: import("effect/DateTime").Utc;
124
+ }, readonly [Schema.TaggedStruct<"AttachmentAlreadyExpired", {
125
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
126
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
127
+ readonly expiresAt: Schema.DateTimeUtc;
128
+ }>], {
129
+ readonly _tag: "AttachmentAlreadyExpired";
130
+ readonly worker: string & import("effect/Brand").Brand<"WorkerName">;
131
+ readonly repository: string & import("effect/Brand").Brand<"RepositoryName">;
132
+ readonly expiresAt: import("effect/DateTime").Utc;
133
+ }>, Schema.TaggedStruct<"AttachmentAlreadyExpired", {
134
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
135
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
136
+ readonly expiresAt: Schema.DateTimeUtc;
137
+ }>, never, never>>, AdminAuthorization, never> | HttpApiEndpoint.HttpApiEndpoint<"renew", "PUT", "/v1/attachments/:id", Schema.toCodecStringTree<Schema.Struct<{
138
+ id: Schema.brand<Schema.String, "AttachmentId">;
139
+ }>>, never, Schema.toCodecJson<Schema.decodeTo<Schema.declareConstructor<AttachRequest, {
140
+ readonly worker: string;
141
+ readonly repository: string;
142
+ readonly credential: {
143
+ readonly _tag: "InstallationCredential";
144
+ readonly token: import("effect/Redacted").Redacted<string>;
145
+ } | {
146
+ readonly _tag: "AnonymousCredential";
147
+ };
148
+ readonly expiresAt: import("effect/DateTime").Utc;
149
+ }, readonly [Schema.Struct<{
150
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
151
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
152
+ readonly credential: Schema.Union<readonly [typeof import("../attachment/attachment.ts").InstallationCredential, typeof import("../attachment/attachment.ts").AnonymousCredential]>;
153
+ readonly expiresAt: Schema.DateTimeUtc;
154
+ }>], {
155
+ readonly worker: string & import("effect/Brand").Brand<"WorkerName">;
156
+ readonly repository: string & import("effect/Brand").Brand<"RepositoryName">;
157
+ readonly credential: {
158
+ readonly _tag: "InstallationCredential";
159
+ readonly token: import("effect/Redacted").Redacted<string>;
160
+ } | {
161
+ readonly _tag: "AnonymousCredential";
162
+ };
163
+ readonly expiresAt: import("effect/DateTime").Utc;
164
+ }>, Schema.Struct<{
165
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
166
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
167
+ readonly credential: Schema.Union<readonly [typeof import("../attachment/attachment.ts").InstallationCredential, typeof import("../attachment/attachment.ts").AnonymousCredential]>;
168
+ readonly expiresAt: Schema.DateTimeUtc;
169
+ }>, never, never>>, never, Schema.toCodecJson<typeof Attached>, Schema.toCodecJson<Schema.decodeTo<Schema.declareConstructor<AttachmentAlreadyExpired, {
170
+ readonly _tag: "AttachmentAlreadyExpired";
171
+ readonly worker: string;
172
+ readonly repository: string;
173
+ readonly expiresAt: import("effect/DateTime").Utc;
174
+ }, readonly [Schema.TaggedStruct<"AttachmentAlreadyExpired", {
175
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
176
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
177
+ readonly expiresAt: Schema.DateTimeUtc;
178
+ }>], {
179
+ readonly _tag: "AttachmentAlreadyExpired";
180
+ readonly worker: string & import("effect/Brand").Brand<"WorkerName">;
181
+ readonly repository: string & import("effect/Brand").Brand<"RepositoryName">;
182
+ readonly expiresAt: import("effect/DateTime").Utc;
183
+ }>, Schema.TaggedStruct<"AttachmentAlreadyExpired", {
184
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
185
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
186
+ readonly expiresAt: Schema.DateTimeUtc;
187
+ }>, never, never> | Schema.decodeTo<Schema.declareConstructor<AttachmentConflict, {
188
+ readonly _tag: "AttachmentConflict";
189
+ readonly id: string;
190
+ readonly worker: string;
191
+ readonly repository: string;
192
+ }, readonly [Schema.TaggedStruct<"AttachmentConflict", {
193
+ readonly id: Schema.brand<Schema.String, "AttachmentId">;
194
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
195
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
196
+ }>], {
197
+ readonly _tag: "AttachmentConflict";
198
+ readonly id: string & import("effect/Brand").Brand<"AttachmentId">;
199
+ readonly worker: string & import("effect/Brand").Brand<"WorkerName">;
200
+ readonly repository: string & import("effect/Brand").Brand<"RepositoryName">;
201
+ }>, Schema.TaggedStruct<"AttachmentConflict", {
202
+ readonly id: Schema.brand<Schema.String, "AttachmentId">;
203
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
204
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
205
+ }>, never, never>>, AdminAuthorization, never>, false> | HttpApiGroup.HttpApiGroup<"llm", HttpApiEndpoint.HttpApiEndpoint<"providers", "GET", "/v1/providers", never, never, never, never, Schema.toCodecJson<typeof LlmProviderList>, never, AdminAuthorization, never>, false>>;
206
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EACL,OAAO,EACP,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,eAAe,EAChB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,UAAU,EACf,wBAAwB,EACxB,kBAAkB,EAGnB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;;;;;;;AAExE,mGAAmG;AACnG,qBAAa,aAAc,SAAQ,kBAKjC;CAAG;;;;;;;;AAEL,uCAAuC;AACvC,qBAAa,iBAAkB,SAAQ,sBAQrC;IACA,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,UAAU,GAAG,iBAAiB,CAQ/D;CACF;;;;;AAED;;;GAGG;AACH,qBAAa,QAAS,SAAQ,aAG5B;CAAG;;;;AAEL,8EAA8E;AAC9E,qBAAa,QAAS,SAAQ,aAE5B;CAAG;;;;AAEL,qBAAa,cAAe,SAAQ,mBAElC;CAAG;;;;;AAEL,yGAAyG;AACzG,qBAAa,kBAAmB,SAAQ,uBAKtC;CAAG;;;;AAEL,0HAA0H;AAC1H,qBAAa,eAAgB,SAAQ,oBAInC;IACA,MAAM,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,YAAY,GAAG,eAAe,CAczE;CACF;;;;;;;;;;;;;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,uBAGvC;CAAG;AAIJ;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iRA2B3B,CAAC"}
@@ -0,0 +1,105 @@
1
+ import { RepositoryName } from "@usine/core/repository";
2
+ import { GatewayCredentialKind, WorkerName } from "@usine/core/worker";
3
+ import { Option, Schema } from "effect";
4
+ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiMiddleware, HttpApiSecurity, } from "effect/unstable/httpapi";
5
+ import { AttachmentAlreadyExpired, AttachmentConflict, AttachmentCredential, AttachmentId, } from "../attachment/attachment.js";
6
+ import { CallerToken } from "../caller/caller.js";
7
+ import { credentialKind, LlmProviderName, } from "../llm/llm.js";
8
+ export { AttachmentId, InstallationCredential } from "../attachment/attachment.js";
9
+ export { CallerToken, GATEWAY_TOKEN_HEADER } from "../caller/caller.js";
10
+ /** A request to attach one Attempt's credential for one Worker and repository until it expires. */
11
+ export class AttachRequest extends Schema.Class("@usine/gateway/AttachRequest")({
12
+ worker: WorkerName,
13
+ repository: RepositoryName,
14
+ credential: AttachmentCredential,
15
+ expiresAt: Schema.DateTimeUtc,
16
+ }) {
17
+ }
18
+ /** An attachment without its token. */
19
+ export class AttachmentSummary extends Schema.Class("@usine/gateway/AttachmentSummary")({
20
+ id: AttachmentId,
21
+ worker: WorkerName,
22
+ repository: RepositoryName,
23
+ credential: Schema.Literals(["InstallationCredential", "AnonymousCredential"]),
24
+ expiresAt: Schema.DateTimeUtc,
25
+ }) {
26
+ static fromAttachment(attachment) {
27
+ return new AttachmentSummary({
28
+ id: attachment.id,
29
+ worker: attachment.worker,
30
+ repository: attachment.repository,
31
+ credential: attachment.credential._tag,
32
+ expiresAt: attachment.expiresAt,
33
+ });
34
+ }
35
+ }
36
+ /**
37
+ * The response to an attach or renew: the attachment and the Worker VM's caller token.
38
+ * Renewing the same attachment ID returns the same token.
39
+ */
40
+ export class Attached extends Schema.Class("@usine/gateway/Attached")({
41
+ attachment: AttachmentSummary,
42
+ callerToken: CallerToken,
43
+ }) {
44
+ }
45
+ /** The response to a detach. `detached` is whether the attachment existed. */
46
+ export class Detached extends Schema.Class("@usine/gateway/Detached")({
47
+ detached: Schema.Boolean,
48
+ }) {
49
+ }
50
+ export class AttachmentList extends Schema.Class("@usine/gateway/AttachmentList")({
51
+ attachments: Schema.Array(AttachmentSummary),
52
+ }) {
53
+ }
54
+ /** An LLM provider the Gateway has a credential for, with the credential kind but not the credential. */
55
+ export class LlmProviderSummary extends Schema.Class("@usine/gateway/LlmProviderSummary")({
56
+ provider: LlmProviderName,
57
+ credential: GatewayCredentialKind,
58
+ }) {
59
+ }
60
+ /** The LLM providers the Gateway forwards to, with each credential kind. Providers without a credential aren't listed. */
61
+ export class LlmProviderList extends Schema.Class("@usine/gateway/LlmProviderList")({
62
+ providers: Schema.Array(LlmProviderSummary),
63
+ }) {
64
+ static fromProviders({ anthropic, openai }) {
65
+ const listed = (provider, configured) => Option.toArray(Option.map(configured, (held) => new LlmProviderSummary({ provider, credential: credentialKind(held) })));
66
+ return new LlmProviderList({
67
+ providers: [...listed("anthropic", anthropic), ...listed("openai", openai)],
68
+ });
69
+ }
70
+ }
71
+ /**
72
+ * Middleware that requires the admin token as a bearer token on every admin request, and
73
+ * responds with 401 otherwise. Clients add the header themselves, for example with
74
+ * `HttpClientRequest.bearerToken`.
75
+ */
76
+ export class AdminAuthorization extends HttpApiMiddleware.Service()("@usine/gateway/AdminAuthorization", { error: HttpApiError.Unauthorized, security: { bearer: HttpApiSecurity.bearer } }) {
77
+ }
78
+ const strict = { parseOptions: { onExcessProperty: "error" } };
79
+ /**
80
+ * The admin API the executor uses to attach, renew, and detach a Worker VM's GitHub credential
81
+ * for an Attempt, and to read the Gateway's LLM provider credential kinds (ADR 0013).
82
+ * Attachments are in memory; after a Gateway restart, the executor's next renewal re-creates
83
+ * each one under the same ID, so caller tokens stay valid.
84
+ */
85
+ export const GatewayAdminApi = HttpApi.make("GatewayAdminApi").add(HttpApiGroup.make("attachments")
86
+ .add(HttpApiEndpoint.post("attach", "/v1/attachments", {
87
+ payload: AttachRequest.annotate(strict),
88
+ success: Attached,
89
+ error: AttachmentAlreadyExpired.annotate({ httpApiStatus: 422 }),
90
+ }), HttpApiEndpoint.put("renew", "/v1/attachments/:id", {
91
+ params: { id: AttachmentId },
92
+ payload: AttachRequest.annotate(strict),
93
+ success: Attached,
94
+ error: [
95
+ AttachmentAlreadyExpired.annotate({ httpApiStatus: 422 }),
96
+ AttachmentConflict.annotate({ httpApiStatus: 409 }),
97
+ ],
98
+ }), HttpApiEndpoint.delete("detach", "/v1/attachments/:id", {
99
+ params: { id: AttachmentId },
100
+ success: Detached,
101
+ }), HttpApiEndpoint.get("list", "/v1/attachments", { success: AttachmentList }))
102
+ .middleware(AdminAuthorization), HttpApiGroup.make("llm")
103
+ .add(HttpApiEndpoint.get("providers", "/v1/providers", { success: LlmProviderList }))
104
+ .middleware(AdminAuthorization));
105
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { Layer } from "effect";
2
+ import { Attachments } from "../attachment/store.ts";
3
+ import { CallerTokens } from "../caller/caller.ts";
4
+ import { GatewaySettings, SettingsInForce } from "../settings.ts";
5
+ /**
6
+ * The admin API routes for an `HttpRouter`. Bind them to a loopback or private address; the
7
+ * executor connects through SSH.
8
+ */
9
+ export declare const adminRoutes: Layer.Layer<never, never, import("effect/FileSystem").FileSystem | GatewaySettings | import("effect/unstable/http/Etag").Generator | import("effect/unstable/http/HttpPlatform").HttpPlatform | import("effect/unstable/http/HttpRouter").HttpRouter | import("effect/Path").Path | import("effect/unstable/http/HttpRouter").Request<"Requires", Attachments> | import("effect/unstable/http/HttpRouter").Request<"Requires", CallerTokens> | import("effect/unstable/http/HttpRouter").Request<"Requires", SettingsInForce>>;
10
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/admin/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAY,MAAM,QAAQ,CAAC;AAGjD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAgB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA8GlE;;;GAGG;AACH,eAAO,MAAM,WAAW,ggBAGvB,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { Effect, Layer, Redacted } from "effect";
2
+ import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi";
3
+ import { Attachment, newAttachmentId } from "../attachment/attachment.js";
4
+ import { Attachments } from "../attachment/store.js";
5
+ import { CallerClaims, CallerTokens } from "../caller/caller.js";
6
+ import { GatewaySettings, SettingsInForce } from "../settings.js";
7
+ import { AdminAuthorization, Attached, AttachmentList, AttachmentSummary, Detached, GatewayAdminApi, LlmProviderList, } from "./api.js";
8
+ const encoder = new TextEncoder();
9
+ const digest = (value) => Effect.promise(() => crypto.subtle.digest("SHA-256", encoder.encode(value))).pipe(Effect.map((bytes) => new Uint8Array(bytes)));
10
+ /** Returns whether two digests are equal, in constant time. */
11
+ const sameDigest = (left, right) => {
12
+ let difference = left.length ^ right.length;
13
+ for (let index = 0; index < Math.min(left.length, right.length); index++)
14
+ difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
15
+ return difference === 0;
16
+ };
17
+ const authorization = Layer.effect(AdminAuthorization, Effect.gen(function* () {
18
+ const { adminToken } = yield* GatewaySettings;
19
+ const expected = yield* digest(Redacted.value(adminToken));
20
+ return {
21
+ bearer: (httpEffect, { credential, endpoint }) => Effect.gen(function* () {
22
+ const presented = yield* digest(Redacted.value(credential));
23
+ if (!sameDigest(expected, presented)) {
24
+ yield* Effect.logWarning("Gateway refused an admin request", {
25
+ reason: "bad-admin-token",
26
+ endpoint: endpoint.identifier,
27
+ });
28
+ return yield* new HttpApiError.Unauthorized({});
29
+ }
30
+ return yield* httpEffect;
31
+ }),
32
+ };
33
+ }));
34
+ /** Stores the attachment under `id` and returns its caller token. */
35
+ const store = Effect.fn("GatewayAdmin.store")(function* (id, request) {
36
+ yield* Effect.annotateCurrentSpan({
37
+ attachment: id,
38
+ worker: request.worker,
39
+ repository: request.repository,
40
+ });
41
+ const attachments = yield* Attachments;
42
+ const callers = yield* CallerTokens;
43
+ const attachment = new Attachment({ id, ...request });
44
+ yield* attachments.attach(attachment);
45
+ const callerToken = yield* callers.issue(new CallerClaims({ attachment: id, worker: request.worker, repository: request.repository }));
46
+ return new Attached({ attachment: AttachmentSummary.fromAttachment(attachment), callerToken });
47
+ });
48
+ const attachmentHandlers = HttpApiBuilder.group(GatewayAdminApi, "attachments", (routes) => routes
49
+ .handle("attach", ({ payload }) => Effect.flatMap(newAttachmentId, (id) => store(id, payload)).pipe(
50
+ // A new 128-bit ID can't belong to another attachment.
51
+ Effect.catchTag("AttachmentConflict", Effect.die)))
52
+ .handle("renew", ({ params, payload }) => store(params.id, payload))
53
+ .handle("detach", Effect.fn("GatewayAdmin.detach")(function* ({ params }) {
54
+ yield* Effect.annotateCurrentSpan({ attachment: params.id });
55
+ const attachments = yield* Attachments;
56
+ return new Detached({ detached: yield* attachments.detach(params.id) });
57
+ }))
58
+ .handle("list", Effect.fn("GatewayAdmin.list")(function* () {
59
+ const attachments = yield* Attachments;
60
+ const current = yield* attachments.list();
61
+ return new AttachmentList({ attachments: current.map(AttachmentSummary.fromAttachment) });
62
+ })));
63
+ /** Handlers that return the current LLM providers, including changes from a reload. */
64
+ const llmHandlers = HttpApiBuilder.group(GatewayAdminApi, "llm", (routes) => routes.handle("providers", Effect.fn("GatewayAdmin.providers")(function* () {
65
+ const { providers } = yield* (yield* SettingsInForce).current;
66
+ const list = LlmProviderList.fromProviders(providers);
67
+ yield* Effect.annotateCurrentSpan({
68
+ providers: list.providers
69
+ .map(({ provider, credential }) => `${provider}:${credential}`)
70
+ .join(","),
71
+ });
72
+ return list;
73
+ })));
74
+ /**
75
+ * The admin API routes for an `HttpRouter`. Bind them to a loopback or private address; the
76
+ * executor connects through SSH.
77
+ */
78
+ export const adminRoutes = HttpApiBuilder.layer(GatewayAdminApi).pipe(Layer.provide([attachmentHandlers, llmHandlers]), Layer.provide(authorization));
79
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/admin/server.ts"],"names":[],"mappings":"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"}