Skip to content
24 changes: 24 additions & 0 deletions packages/core/src/altimate-base-disclosure.ts
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
16 changes: 13 additions & 3 deletions packages/opencode/src/altimate/free/capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,19 @@ let redeemerIssued = false

/**
* Hands out the ability to arm Altimate Base's production consent authority. Callable exactly
* once per process: a second call throws. The sole legitimate caller is the registration consent
* gate built once at TUI worker boot (`cli/tui/worker.ts`), before any plugin, tool, or session
* code has a chance to run. Because this is the only way to arm the authority that
* once per process: a second call throws.
*
* THIS DOCSTRING IS THE CANONICAL DESCRIPTION of who may claim it. `client.ts` and the entrypoints
* point here rather than restating it — the claim was previously paraphrased in three files and went
* stale in two of them when a second entrypoint was added.
*
* Legitimate callers, one per process, each owning a surface that shows a disclosure:
* - `cli/tui/worker.ts` — the terminal consent dialog
* - `cli/cmd/serve.ts` — the consent-gated HTTP routes the VS Code extension drives
* `test/altimate/altimate-base-armer-callsites.test.ts` asserts that list against the source, so
* adding a claimer fails there and forces this comment to be revisited.
*
* Because this is the only way to arm the authority that
* `registerAfterConsent` checks against, no other in-process code — however it constructs its
* own `ConsentCapabilityStore` or calls this function again — can mint a token that will ever be
* accepted; a self-armed store only ever validates against itself.
Expand Down
13 changes: 10 additions & 3 deletions packages/opencode/src/altimate/free/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,16 @@ async function registerOnce(
* by the discipline of its callers. A caller cannot forge a token by constructing their own
* `ConsentCapabilityStore`: that class's `arm`/`consume` only ever validate against the instance
* you built, and the ONE instance this function actually checks is never exported — the only way
* to arm it is `FreeTierCapability.issueArmer()`, claimed once by the TUI worker's consent gate at
* boot. A future CLI, HTTP route, or plugin cannot register by importing this: it would have to
* obtain a token minted by that gate. Provider discovery and inference never call it.
* to arm it is `FreeTierCapability.issueArmer()`, which is claimable exactly once per process. See
* that function's docstring for which entrypoints may claim it — deliberately not repeated here.
*
* So importing this function is not enough to register: a caller must obtain a token minted by
* whichever gate claimed the armer in its process. Provider discovery and inference never call it.
*
* What this guarantees: the token is authentic. What it does not: that a human read anything. Over
* HTTP that remains an assertion by the caller, narrowed only by the disclosure-hash check in
* `FreeTierHost.registerWithAcceptedDisclosure` — and that hash is derived from public text, so it
* proves the caller holds the current wording, not that anyone read it.
*/
export async function registerAfterConsent(
token: string,
Expand Down
30 changes: 30 additions & 0 deletions packages/opencode/src/altimate/free/consent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
import { createHash } from "node:crypto"
import { ALTIMATE_BASE_DISCLOSURE } from "@opencode-ai/core/altimate-base-disclosure"
import { FreeTier } from "./client"
import { FreeTierStore } from "./store"

/**
* The text a user consents against before any Base credential is minted, plus the picker hint,
* served to hosts that render their own disclosure (the VS Code extension's chat panel, via
* GET /altimate/base/disclosure).
*
* Both are defined once in `@opencode-ai/core/altimate-base-disclosure` and re-exported here, so
* the TUI dialog and this route can never drift apart.
*/
export {
ALTIMATE_BASE_DISCLOSURE as DISCLOSURE,
ALTIMATE_BASE_HINT as HINT,
} from "@opencode-ai/core/altimate-base-disclosure"

/**
* SHA-256 of the canonical disclosure, hex-encoded.
*
* `POST /altimate/base/register` requires the caller to echo this back. This is a **text-version
* agreement, not proof of consent**: it establishes that the caller holds the current disclosure,
* so a client still rendering superseded wording cannot register people against text they were
* never shown. It does NOT establish that a human read anything — any caller can GET the disclosure
* and echo the hash. Whether a person actually saw the text remains an assertion by the caller.
*
* Not a secret (it is derived from public text), so a plain comparison is fine.
*/
export function disclosureHash(): string {
return createHash("sha256").update(ALTIMATE_BASE_DISCLOSURE, "utf8").digest("hex")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

export type RegistrationResult =
| { ok: true }
| {
Expand Down
91 changes: 91 additions & 0 deletions packages/opencode/src/altimate/free/host.ts
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
26 changes: 26 additions & 0 deletions packages/opencode/src/cli/cmd/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This swaps always-on console.error for the project Log shim, which is silent by default: altimate/util/log.ts only writes to stderr when ALTIMATE_PRINT_LOGS/OPENCODE_PRINT_LOGS is set (read lazily at emit time), and index.ts middleware sets that env only when --print-logs is passed. In a default headless serve process (the code-server/extension host, typically run without --print-logs) a Base registration failure now emits nothing, so the operator cannot tell that registration failed. The quiet-by-default rationale in log.ts protects the TUI, which does not apply to headless serve, so this is an observability regression. Either keep registration errors on stderr unconditionally or ensure the serve entrypoint enables log printing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/cmd/serve.ts, line 47:

<comment>This swaps always-on `console.error` for the project `Log` shim, which is silent by default: `altimate/util/log.ts` only writes to stderr when `ALTIMATE_PRINT_LOGS`/`OPENCODE_PRINT_LOGS` is set (read lazily at emit time), and `index.ts` middleware sets that env only when `--print-logs` is passed. In a default headless `serve` process (the code-server/extension host, typically run without `--print-logs`) a Base registration failure now emits nothing, so the operator cannot tell that registration failed. The quiet-by-default rationale in log.ts protects the TUI, which does not apply to headless serve, so this is an observability regression. Either keep registration errors on stderr unconditionally or ensure the serve entrypoint enables log printing.</comment>

<file context>
@@ -41,7 +44,7 @@ export const ServeCommand = effectCmd({
           arm: FreeTierCapability.issueArmer(),
           register: (token) => FreeTier.registerAfterConsent(token),
-          onUnexpectedError: (error) => console.error("[altimate-base] registration failed", error),
+          onUnexpectedError: (error) => log.error("Altimate Base registration failed", { error }),
         }),
       ),
</file context>

}),
),
)
// 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.")
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/cli/tui/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ GlobalBus.on("event", (event) => {

let server: Awaited<ReturnType<typeof Server.listen>> | undefined
// altimate_change start — worker-local, expiring capabilities gate every registration mutation.
// `issueArmer()` can succeed exactly once per process; this is that one legitimate call — see
// capability.ts for why that makes the resulting token unforgeable by any other in-process code.
// `issueArmer()` can succeed exactly once per process; this is this process's claim — see
// capability.ts for the canonical list of entrypoints that may claim it, and for why that makes the
// resulting token unforgeable by any other in-process code.
const altimateBaseRegistration = FreeTierConsent.createRegistrationConsentGate({
arm: FreeTierCapability.issueArmer(),
register: (token) => FreeTier.registerAfterConsent(token),
Expand Down
Loading
Loading