mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 09:15:19 +03:00
docs: this repo is public, so say what things are, not who decided them
Comments and served API descriptions here had grown references that only make sense to someone with our internal notes: relative paths that escape this tree, filenames and titles of documents nobody outside can open, quoted prose from them, and the name of a component that has no public surface — once in an OpenAPI description, which is published output rather than source. None of it was load-bearing. Every case restates as what the code actually requires, and every rewrite came out shorter: "in the words the host agent reports" for a component name, "republished as addresses are discovered" for a quoted phrase, "a size tier sets vCPU, RAM and the output geometry" for a sentence that had been carrying a path. Internal reasoning is now cited exactly one way, ref(d-NNNN) in a source comment, with the rule that the sentence must still stand if the marker is deleted. CLAUDE.md leads with it, because the previous version of this mistake was made by people who knew the repo was public and it still took ten occurrences to notice, so "be careful" is not a mechanism. Commit messages get the stricter rule and carry no references at all: a comment can be fixed by the next commit and a published message cannot be fixed at all. Git hooks now enforce both halves. The check caught a real one while being written: the CLAUDE.md table spelled out the paths it was prohibiting, which discloses them to exactly the reader it protects against. 138 tests, 0 fail.
This commit is contained in:
@@ -51,9 +51,9 @@ const Machine = z.object({
|
||||
properties: z.object({
|
||||
machineID: z.string(),
|
||||
ownerUserID: z.string(),
|
||||
// Not optional: `machine.teamId` is notNull since 0048, so a host that
|
||||
// authenticated always has a team and the branch that used to handle
|
||||
// its absence was handling a state that can no longer exist.
|
||||
// Not optional: `machine.teamId` is notNull, so a host that authenticated
|
||||
// always has a team, and the branch that used to handle its absence was
|
||||
// handling a state that can no longer exist.
|
||||
teamID: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -7,41 +7,38 @@ import { UserTable } from '../user/user.sql.js';
|
||||
/**
|
||||
* The named size a box was asked for.
|
||||
*
|
||||
* A tier is the unit of sale and it includes output geometry
|
||||
* ([0021](../../../../.nestri/decisions/0021-vm-size-tiers.md)), so this column
|
||||
* decides vCPU, RAM *and* the resolution the guest is told to render at. It is
|
||||
* a *request*: `(tier, gpu_model)` is what admission actually acts on, and
|
||||
* admission does not exist yet.
|
||||
* A size tier is the unit of sale and it includes output geometry, so this
|
||||
* column decides vCPU, RAM *and* the resolution the guest is told to render
|
||||
* at. It is a *request*: what gets admitted is the pair of tier and GPU model,
|
||||
* and admission does not exist yet. ref(d-0021)
|
||||
*/
|
||||
export const BoxTier = pgEnum('box_tier', ['xs', 'sm', 'md', 'lg', 'xl']);
|
||||
|
||||
/**
|
||||
* What the box is doing, in `neslet`'s own vocabulary.
|
||||
* What the box is doing, in the words the host agent reports.
|
||||
*
|
||||
* Deliberately the same three states `neslet` reports over its control socket
|
||||
* and no more. `starting` and `stopping` are the obvious additions and both are
|
||||
* omitted, because nothing would ever write them: the agent's transitions are
|
||||
* synchronous from its side, so a state nobody sets is a state that lies. A box
|
||||
* that failed is `stopped` with `stopClean` false, which is also how `neslet`
|
||||
* models it — "it is not running" and "it faulted forty seconds ago" are
|
||||
* different facts, and the difference lives in the reason, not in the state.
|
||||
* Deliberately the three states an agent actually reports and no more.
|
||||
* `starting` and `stopping` are the obvious additions and both are omitted,
|
||||
* because nothing would ever write them: those transitions are synchronous
|
||||
* from the agent's side, so a state nobody sets is a state that lies. A box
|
||||
* that failed is `stopped` with `stopClean` false — "it is not running" and
|
||||
* "it faulted forty seconds ago" are different facts, and the difference lives
|
||||
* in the reason, not in the state.
|
||||
*/
|
||||
export const BoxState = pgEnum('box_state', ['created', 'running', 'stopped']);
|
||||
|
||||
/**
|
||||
* A VM someone owns.
|
||||
*
|
||||
* Nothing represented a box until now
|
||||
* ([0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md)):
|
||||
* `machine` is the *host*, and a guest had no id a URL could carry, no owner,
|
||||
* no place, and no state anything could poll. Every screen the desktop app
|
||||
* still needs is a view over this table.
|
||||
* Nothing represented a box until now: `machine` is the *host*, and a guest
|
||||
* had no id a URL could carry, no owner, no place, and no state anything could
|
||||
* poll. Every screen the desktop app still needs is a view over this table.
|
||||
*
|
||||
* **A box is owned by a person and placed on a team's hardware, and those are
|
||||
* two different relationships.** Hence both `userId` and `machineId`: the
|
||||
* person is who it belongs to and who gets billed through its sessions, the
|
||||
* machine is where it currently runs. Moving a box to another host changes the
|
||||
* second and not the first.
|
||||
* second and not the first. ref(d-0048)
|
||||
*/
|
||||
export const BoxTable = pgTable(
|
||||
'box',
|
||||
@@ -57,13 +54,13 @@ export const BoxTable = pgTable(
|
||||
machineId: ulid('machine_id')
|
||||
.notNull()
|
||||
.references(() => MachineTable.id, { onDelete: 'restrict' }),
|
||||
// The DNS label is the id, per 0019 — `<box_id>.nestri.link`. This is the
|
||||
// display string a person edits, and it is deliberately not unique:
|
||||
// two boxes called "living room" are the owner's problem, not an error.
|
||||
// The id is the DNS label; this is the display string a person edits, and
|
||||
// it is deliberately not unique: two boxes called "living room" are the
|
||||
// owner's problem, not an error. ref(d-0019)
|
||||
label: text('label').notNull(),
|
||||
tier: BoxTier('tier').notNull().default('sm'),
|
||||
state: BoxState('state').notNull().default('created'),
|
||||
/** Why it stopped, verbatim from `neslet`. Null while it has never run. */
|
||||
/** Why it stopped, verbatim from the agent. Null while it has never run. */
|
||||
stopReason: text('stop_reason'),
|
||||
/** Whether that stop was a clean exit. Null while it has never run. */
|
||||
stopClean: boolean('stop_clean')
|
||||
|
||||
@@ -9,11 +9,10 @@ import { BoxState, BoxTable, BoxTier } from './box.sql.js';
|
||||
/**
|
||||
* A VM someone owns.
|
||||
*
|
||||
* The box is the thing with a name and a URL ([0010](../../../../.nestri/decisions/0010-the-name-is-the-interface.md),
|
||||
* [0019](../../../../.nestri/decisions/0019-box-naming.md)); a
|
||||
* {@link ../session/index.ts | session} is one run of it, and the session is
|
||||
* what costs money. Keeping them apart is what lets a box be a durable thing a
|
||||
* person owns rather than a synonym for "currently playing".
|
||||
* The box is the thing with a name and a URL; a session is one run of it, and
|
||||
* the session is what costs money. Keeping them apart is what lets a box be a
|
||||
* durable thing a person owns rather than a synonym for "currently playing".
|
||||
* ref(d-0010), ref(d-0019)
|
||||
*/
|
||||
export namespace Box {
|
||||
export const Info = z
|
||||
@@ -39,11 +38,11 @@ export namespace Box {
|
||||
example: Examples.Box.tier
|
||||
}),
|
||||
state: z.enum(BoxState.enumValues).meta({
|
||||
description: 'What the box is doing, in neslet’s vocabulary',
|
||||
description: 'What the box is doing, as last reported by its host',
|
||||
example: Examples.Box.state
|
||||
}),
|
||||
stopReason: z.string().nullable().optional().meta({
|
||||
description: 'Why it stopped, verbatim from neslet. Null if it never ran',
|
||||
description: 'Why it stopped, as reported by its host. Null if it never ran',
|
||||
example: Examples.Box.stopReason
|
||||
}),
|
||||
stopClean: z.boolean().nullable().optional().meta({
|
||||
@@ -114,7 +113,7 @@ export namespace Box {
|
||||
});
|
||||
|
||||
/**
|
||||
* Record what `neslet` says a box is doing.
|
||||
* Record what a box's host says it is doing.
|
||||
*
|
||||
* The stop reason is cleared on any state that is not `stopped`, so a box
|
||||
* that ran, faulted, and was started again does not keep explaining a
|
||||
|
||||
@@ -6,16 +6,15 @@ import { User } from '../user/index.js';
|
||||
import { LinkedAccount } from '../user/linked-account.js';
|
||||
|
||||
/**
|
||||
* Fixtures for the ownership chain, because since
|
||||
* [0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md)
|
||||
* it is a chain rather than a set of loose rows.
|
||||
* Fixtures for the ownership chain, which is now a chain rather than a set of
|
||||
* loose rows.
|
||||
*
|
||||
* A box now needs a user, a team, and a machine to exist before it can, and a
|
||||
* A box needs a user, a team, and a machine to exist before it can, and a
|
||||
* session needs a game and a linked account on top of that. Every test that
|
||||
* touches either was otherwise going to build the same four rows by hand, and
|
||||
* the version built by hand is the version that quietly uses a `hst_…` string
|
||||
* where a real machine id belongs — which is exactly what the new foreign key
|
||||
* exists to catch.
|
||||
* the version built by hand is the version that quietly uses a made-up host
|
||||
* string where a real machine id belongs — which is exactly what the foreign
|
||||
* key exists to catch. ref(d-0048)
|
||||
*
|
||||
* Test-only. Nothing here is imported by shipping code.
|
||||
*/
|
||||
|
||||
@@ -17,9 +17,9 @@ export const GameDownloadTable = pgTable(
|
||||
{
|
||||
...id,
|
||||
...timestamps,
|
||||
// A foreign key since 0048. It was a bare `text` column — the one place a
|
||||
// host was referred to by a string nothing checked — so a typo produced
|
||||
// a download row belonging to a machine that had never existed.
|
||||
// A foreign key. It was a bare `text` column — the one place a host was
|
||||
// referred to by a string nothing checked — so a typo produced a download
|
||||
// row belonging to a machine that had never existed. ref(d-0048)
|
||||
hostId: ulid('host_id')
|
||||
.notNull()
|
||||
.references(() => MachineTable.id, { onDelete: 'cascade' }),
|
||||
|
||||
@@ -9,9 +9,9 @@ import { GameDownload } from './download.js';
|
||||
const sql = testDb();
|
||||
|
||||
/**
|
||||
* Real registered hosts, not `hst_…` strings.
|
||||
* Real registered hosts, not made-up strings.
|
||||
*
|
||||
* These were literals until 0048 made `host_id` a foreign key. The old values
|
||||
* These were literals until `host_id` became a foreign key. The old values
|
||||
* were the bug the key exists to prevent — a download row attributed to a host
|
||||
* that had never registered — so the test that used them was asserting against
|
||||
* a state the database now refuses.
|
||||
|
||||
@@ -133,11 +133,10 @@ export namespace Machine {
|
||||
/**
|
||||
* Move a host to a different team.
|
||||
*
|
||||
* There is no "out of a team" any more: `teamId` is notNull since
|
||||
* [0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md),
|
||||
* so a host always belongs to exactly one, and the single-operator case is a
|
||||
* team of one rather than a null. What used to be *unscope* is now *move to
|
||||
* my personal team*, which the caller names explicitly.
|
||||
* There is no "out of a team" any more: `teamId` is notNull, so a host always
|
||||
* belongs to exactly one and the single-operator case is a team of one rather
|
||||
* than a null. What used to be *unscope* is now *move to my personal team*,
|
||||
* which the caller names explicitly. ref(d-0048)
|
||||
*
|
||||
* Scoped to the owner in the query itself, so a machine belonging to
|
||||
* someone else is a miss rather than a permission check that could be
|
||||
|
||||
@@ -23,8 +23,8 @@ export const MachineTable = pgTable(
|
||||
.references(() => UserTable.id, { onDelete: 'cascade' }),
|
||||
// Every user gets a personal team at signup, so there is always one to
|
||||
// point at and the single-operator case is a team of one rather than a
|
||||
// special case in every query. This was nullable until 0048, which cost
|
||||
// a `teamId ?? ownerUserId` branch at each call site instead.
|
||||
// special case in every query. This was nullable, which cost a
|
||||
// `teamId ?? ownerUserId` branch at each call site instead. ref(d-0048)
|
||||
teamId: ulid('team_id')
|
||||
.notNull()
|
||||
.references(() => TeamTable.id, { onDelete: 'restrict' }),
|
||||
|
||||
@@ -62,8 +62,8 @@ describe('Machine registration', () => {
|
||||
|
||||
test('a host always has a team, so registering without one is impossible', async () => {
|
||||
const owner = await newOwner('mch-team');
|
||||
// `teamId` is notNull since 0048 and required by the schema, so this is a
|
||||
// validation failure rather than a row with a null team.
|
||||
// `teamId` is notNull and required by the schema, so this is a validation
|
||||
// failure rather than a row with a null team.
|
||||
//
|
||||
// `toThrow` and not `rejects.toThrow`: `fn()` parses its input
|
||||
// synchronously, before any promise exists, so a bad argument never
|
||||
|
||||
@@ -136,9 +136,9 @@ export namespace Session {
|
||||
/**
|
||||
* Publish the current ticket.
|
||||
*
|
||||
* Overwrites, deliberately: the vsock contract describes the ticket as *"a
|
||||
* stream, not one value"*, so a later ticket for the same session is a
|
||||
* better address for the same thing and not a second session.
|
||||
* Overwrites, deliberately: a ticket is republished as addresses are
|
||||
* discovered, so a later one for the same session is a better address for
|
||||
* the same thing and not a second session.
|
||||
*/
|
||||
export const setTicket = fn(Info.pick({ id: true, ticket: true }), async (input) => {
|
||||
return Database.use(async (tx) => {
|
||||
|
||||
@@ -10,7 +10,8 @@ import { LinkedAccountTable } from '../user/linked-account.sql.js';
|
||||
*
|
||||
* `requested` is written by `POST /session` before anything has been placed,
|
||||
* which is what makes the row the job: the control plane picks a machine and
|
||||
* `neslet` takes it from here. `live` is the only state that costs money.
|
||||
* the agent on it takes over from there. `live` is the only state that costs
|
||||
* money.
|
||||
*/
|
||||
export const SessionState = pgEnum('session_state', [
|
||||
'requested',
|
||||
@@ -23,14 +24,12 @@ export const SessionState = pgEnum('session_state', [
|
||||
/**
|
||||
* One live run of one box, and the thing that gets billed.
|
||||
*
|
||||
* Separate from `box` for two reasons
|
||||
* ([0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md)):
|
||||
* a box is a durable thing somebody owns while a session is what costs money
|
||||
* and what [`limits.md`](../../../../.nestri/contracts/limits.md) burns
|
||||
* session-hours against — and because the connect ticket **changes after bind
|
||||
* as addresses are discovered.** The vsock contract calls it *"a stream, not
|
||||
* one value"*, so `ticket` is a column that gets rewritten in place while the
|
||||
* session is starting, and a client polls it rather than receiving it once.
|
||||
* Separate from `box` for two reasons. A box is a durable thing somebody owns,
|
||||
* while a session is what costs money and what quota is measured in
|
||||
* session-hours against. And the connect ticket **changes after bind as
|
||||
* addresses are discovered** — it is republished rather than issued once, so
|
||||
* `ticket` is a column rewritten in place while the session starts and a
|
||||
* client polls it instead of receiving a final value. ref(d-0048)
|
||||
*/
|
||||
export const SessionTable = pgTable(
|
||||
'session',
|
||||
@@ -43,9 +42,9 @@ export const SessionTable = pgTable(
|
||||
gameId: ulid('game_id')
|
||||
.notNull()
|
||||
.references(() => GameTable.id, { onDelete: 'restrict' }),
|
||||
// Which Steam account this run is playing as. A user may have up to four
|
||||
// linked, and *which one* is the question the "who's playing?" screen
|
||||
// asks — so it belongs on the session and not on the box.
|
||||
// Which Steam account this run is playing as. A user may link several,
|
||||
// and *which one* is the question the "who's playing?" screen asks — so
|
||||
// it belongs on the session and not on the box.
|
||||
//
|
||||
// `restrict`, because unlinking a Steam account must not erase the
|
||||
// billing history of what it played.
|
||||
@@ -67,8 +66,8 @@ export const SessionTable = pgTable(
|
||||
(t) => [
|
||||
index('session_box_idx').on(t.boxId),
|
||||
index('session_state_idx').on(t.state),
|
||||
// Metering reads "sessions in this window"; per 0048 this table is what
|
||||
// billing sums, so the time index is not speculative.
|
||||
// Metering reads "sessions in this window", and this table is what
|
||||
// billing sums, so the time index is not speculative. ref(d-0048)
|
||||
index('session_started_idx').on(t.timeStarted)
|
||||
]
|
||||
);
|
||||
|
||||
@@ -87,8 +87,8 @@ describe('Session', () => {
|
||||
expect((await Session.setTicket({ id: session.id, ticket: 'ticket-one' }))?.ticket).toBe(
|
||||
'ticket-one'
|
||||
);
|
||||
// The vsock contract calls the ticket "a stream, not one value" — a second
|
||||
// ticket is a better address for the same session, not a new session.
|
||||
// A ticket is republished as addresses are discovered — a second one is a
|
||||
// better address for the same session, not a new session.
|
||||
expect((await Session.setTicket({ id: session.id, ticket: 'ticket-two' }))?.ticket).toBe(
|
||||
'ticket-two'
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ export namespace Team {
|
||||
* only thing that mints a team at signup — so the first one is the personal
|
||||
* one and any later ones were made deliberately. This is a convention, not a
|
||||
* column: adding an `isPersonal` flag would let the two disagree, and there
|
||||
* is nothing yet that needs them to.
|
||||
* is nothing yet that needs them to. ref(d-0048)
|
||||
*/
|
||||
export const personalFor = fn(Info.shape.ownerId, async (ownerId) => {
|
||||
return Database.use(async (tx) => {
|
||||
@@ -136,11 +136,10 @@ export namespace Team {
|
||||
/**
|
||||
* The personal team, made if it is not there.
|
||||
*
|
||||
* Every user has needed one since [0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md)
|
||||
* made `machine.teamId` notNull, so signup calls this and so does anything
|
||||
* that needs somewhere to put a host. Idempotent, because it runs on every
|
||||
* login rather than only on the first one — a user created before 0048 has
|
||||
* no team and gets one the next time they appear.
|
||||
* Every user has needed one since `machine.teamId` became notNull, so signup
|
||||
* calls this and so does anything that needs somewhere to put a host.
|
||||
* Idempotent, because it runs on every login rather than only on the first
|
||||
* one — an older user with no team gets one the next time they appear.
|
||||
*/
|
||||
export const ensurePersonal = fn(z.object({ displayName: z.string() }), async (input) => {
|
||||
const existing = await personalFor(Actor.userID);
|
||||
|
||||
@@ -54,8 +54,8 @@ describe('Team.ensurePersonal', () => {
|
||||
});
|
||||
|
||||
test('a user who predates the personal team gets one on next login', async () => {
|
||||
// The legacy row the 0007 migration and this call between them repair: a
|
||||
// user created by Steam sign-in before `ensurePersonal` was ever wired up.
|
||||
// The legacy row the migration and this call between them repair: a user
|
||||
// created by Steam sign-in before `ensurePersonal` was ever wired up.
|
||||
const userId = Identifier.ascending('user');
|
||||
createdUserIds.push(userId);
|
||||
await sql`insert into "user" (id, name, email) values (${userId}, ${'legacy'}, ${`legacy-${userId}@example.test`})`;
|
||||
|
||||
Reference in New Issue
Block a user