-
Notifications
You must be signed in to change notification settings - Fork 134
feat: expose Altimate Base registration over HTTP for non-TUI hosts #1266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
977bb28
cd8b94b
e2e3aa2
b2c2824
6eb2b09
3692084
6d58266
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // altimate_change start — the single definition of the Altimate Base consent disclosure. | ||
| // | ||
| // This text is what a user actually consents against before any Base credential is minted, so it | ||
| // must be identical everywhere it is shown. Two packages render it and neither can import from the | ||
| // other: the TUI's disclosure dialog (`packages/tui`) and the HTTP disclosure route that serves | ||
| // hosts rendering their own dialog (`packages/opencode`, for the VS Code extension's chat panel). | ||
| // `packages/core` is the only module both already depend on, so the constant lives here. | ||
| // | ||
| // A new leaf file rather than an addition to an existing core module, so it adds no upstream | ||
| // rebase surface. | ||
| // | ||
| // It states the core data terms up front. The persistent per-install-id linkage detail is | ||
| // disclosed in docs/docs/configure/providers.md ("Data handling") rather than repeated in the | ||
| // gate (see #1268); keep the core terms in sync with that note. | ||
| export const ALTIMATE_BASE_DISCLOSURE = | ||
| "Altimate Base is free and requires no signup. Requests and responses may be logged and used to improve Altimate's products, including the model. Secrets are automatically masked before storage, but don't rely on it — avoid sending secrets or confidential code. Usage can be rate limited." | ||
|
|
||
| /** | ||
| * The one-line subtitle shown next to Altimate Base in a provider or model picker. Shared for the | ||
| * same reason as the disclosure: it previously existed in three drifting variants across the TUI | ||
| * pickers and the extension. | ||
| */ | ||
| export const ALTIMATE_BASE_HINT = "free · no signup · rate limited" | ||
| // altimate_change end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // altimate_change start — host-injected Altimate Base registration for non-TUI entrypoints. | ||
| // | ||
| // `FreeTierCapability.issueArmer()` is claimable exactly once per process and throws on a second | ||
| // call, so an HTTP route cannot claim one for itself: the TUI worker already claims it at boot for | ||
| // its own RPC gate, and that worker also serves HTTP from the same process. A route-level claim | ||
| // would therefore break the TUI worker the moment the routes module loaded — and any test that | ||
| // imported both the server and the Base test harness. | ||
| // | ||
| // Instead the entrypoint that owns the process claims the capability once and hands the resulting | ||
| // gate here. This is the same shape the TUI already uses for the same operation | ||
| // (`packages/tui/src/context/altimate-base-consent.tsx`): the host injects, the consumer checks. | ||
| // | ||
| // `altimate serve` provides a gate. The TUI worker deliberately does NOT — the TUI owns its own | ||
| // disclosure dialog, and a second registration surface inside that process would let a caller | ||
| // register without the dialog ever being shown. Consumers must treat "cannot register" as final | ||
| // and refuse, exactly as the TUI's provider picker does. | ||
| // | ||
| // The gate itself is NEVER handed back out. An earlier revision exposed `current()`, which returned | ||
| // the whole gate — including `setToken` (closing over the real armer) and `register` (redeeming | ||
| // against the real authority) — so any importer held a raw mint primitive and could register | ||
| // without going near the disclosure, in any order it liked. The check now happens *inside* this | ||
| // module, in the same call that mints, arms and redeems: there is no ordering for a caller to get | ||
| // wrong and no primitive to borrow. | ||
| // | ||
| // What this is NOT: a trust boundary against in-process code. `registerWithAcceptedDisclosure` is | ||
| // exported, and the hash it demands is a SHA-256 of public text that any caller can recompute via | ||
| // `FreeTierConsent.disclosureHash()`. In-process code can therefore still cause a registration — | ||
| // it simply cannot do so while bypassing the documented precondition, and there is now one | ||
| // audited path instead of a capability handed to every importer. The real boundary is the process: | ||
| // anything running here is already trusted to execute tools. What this closes is accidental | ||
| // misuse and the drift that comes from re-implementing the check at each call site. | ||
| import { randomBytes } from "node:crypto" | ||
| import type { createRegistrationConsentGate, RegistrationResult } from "./consent" | ||
| import { FreeTierConsent } from "./consent" | ||
|
|
||
| export type Registration = ReturnType<typeof createRegistrationConsentGate> | ||
|
|
||
| let registration: Registration | undefined | ||
|
|
||
| /** | ||
| * Install the process's registration gate. Called once by the entrypoint, before the server starts | ||
| * accepting requests. | ||
| * | ||
| * Single-shot, matching every other capability in this area: a second call throws rather than | ||
| * silently replacing the gate. The earlier last-write-wins behaviour let any in-process caller swap | ||
| * the gate out from under the routes after `serve` installed the real one. That was never a | ||
| * privilege escalation — such code is already trusted and still cannot forge a token the private | ||
| * authority accepts — but it was a weaker invariant than `issueArmer()`/`issueRedeemer()` next door, | ||
| * for no benefit. | ||
| */ | ||
| export function provide(value: Registration): void { | ||
| if (registration) throw new Error("Altimate Base registration gate already provided for this process") | ||
| registration = value | ||
| } | ||
|
|
||
| /** Whether this host can register Altimate Base at all, i.e. whether an entrypoint provided a gate. */ | ||
| export function canRegister(): boolean { | ||
| return registration !== undefined | ||
| } | ||
|
|
||
| export type RegisterOutcome = | ||
| /** No gate was provided; this host cannot register Altimate Base. */ | ||
| | { kind: "unavailable" } | ||
| /** The caller echoed a hash that is not the current disclosure's. */ | ||
| | { kind: "staleDisclosure" } | ||
| /** The gate ran; `result` carries its success or its classified failure. */ | ||
| | { kind: "done"; result: RegistrationResult } | ||
|
|
||
| /** | ||
| * Verify the caller accepted the current disclosure text, then mint, arm and redeem in one step. | ||
| * | ||
| * The hash comparison lives here rather than in the caller so that holding the current disclosure | ||
| * text is a precondition of minting, not a convention the caller is trusted to follow. It is a | ||
| * **text-version agreement, not proof of consent**: it establishes that the caller holds the | ||
| * current wording, so a client still rendering superseded text cannot register people against text | ||
| * they were never shown. Any caller can fetch the disclosure and echo the hash, so "a human read | ||
| * this" remains an assertion by the caller. | ||
| */ | ||
| export async function registerWithAcceptedDisclosure(acceptedDisclosureSha256: string): Promise<RegisterOutcome> { | ||
| const gate = registration | ||
| if (!gate) return { kind: "unavailable" } | ||
| if (acceptedDisclosureSha256.toLowerCase() !== FreeTierConsent.disclosureHash()) { | ||
| return { kind: "staleDisclosure" } | ||
| } | ||
| const token = randomBytes(32).toString("hex") | ||
| gate.setToken({ token }) | ||
| return { kind: "done", result: await gate.register({ token }) } | ||
| } | ||
|
|
||
| export * as FreeTierHost from "./host" | ||
| // altimate_change end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,17 @@ import { subscribeTraceConsumer } from "../../altimate/observability/trace-consu | |
| // altimate_change start — self-update on headless serve startup | ||
| import { scheduleStartupUpgradeCheck } from "./serve-upgrade-check" | ||
| // altimate_change end | ||
| // altimate_change start — Altimate Base registration capability for the headless server | ||
| import { FreeTier } from "../../altimate/free/client" | ||
| import { FreeTierCapability } from "../../altimate/free/capability" | ||
| import { FreeTierConsent } from "../../altimate/free/consent" | ||
| import { FreeTierHost } from "../../altimate/free/host" | ||
| import { Log } from "../../util/log" | ||
| // altimate_change end | ||
|
|
||
| // altimate_change start — logger for the Base registration gate's onUnexpectedError hook | ||
| const log = Log.create({ service: "serve" }) | ||
| // altimate_change end | ||
|
|
||
| export const ServeCommand = effectCmd({ | ||
| command: "serve", | ||
|
|
@@ -25,6 +36,21 @@ export const ServeCommand = effectCmd({ | |
| // because it must be readable from every module realm. | ||
| process.env["ALTIMATE_CODE_SERVE"] = "1" | ||
| // altimate_change end | ||
| // altimate_change start — claim the process's one Altimate Base consent capability here, at the | ||
| // entrypoint, before the server can accept a request. `serve` is the extension's host and has no | ||
| // TUI to show the disclosure dialog, so the disclosure + registration routes are how a Base | ||
| // credential gets minted in this process. Claiming it here (rather than in the routes module) | ||
| // keeps the TUI worker — which claims the same capability for its own dialog — unaffected. | ||
| yield* Effect.sync(() => | ||
| FreeTierHost.provide( | ||
| FreeTierConsent.createRegistrationConsentGate({ | ||
| arm: FreeTierCapability.issueArmer(), | ||
| register: (token) => FreeTier.registerAfterConsent(token), | ||
| onUnexpectedError: (error) => log.error("Altimate Base registration failed", { error }), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This swaps always-on Prompt for AI agents |
||
| }), | ||
| ), | ||
| ) | ||
| // altimate_change end | ||
| const { Server } = yield* Effect.promise(() => import("../../server/server")) | ||
| if (!Flag.OPENCODE_SERVER_PASSWORD) { | ||
| console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.