Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,8 @@ outside it moves to its own issue and pull request.
`~/.local/share/stim/services/<label>/versions/` on Linux, keeping the
running and the previous version, and repoint only the service's server
script; a failed health check switches back. A Linux worker takes Android
builds only; setup there approves `build` and refuses `device-host`. A client approved for `build` or
builds only; setup there approves `build` and refuses `device-host`, and off macOS
stim-server refuses device-host requests and grants. A client approved for `build` or
`device-host` can run the same update on this machine over the tailnet, and no
other connection can. An exact npm release needs no setting; a client's own
packages are taken only while `server.acceptClientBuilds` is `true`, its
Expand Down
3 changes: 2 additions & 1 deletion packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ Argument and preflight refusals change nothing. Preflight requires macOS or
Linux, Node 22.12+, running Tailscale, the client node on this tailnet, and
this user's GUI login session on macOS or systemd user manager on Linux. On
Linux, setup takes `--build` only and refuses `--device-host`, because hosting
needs a Mac; see [On Linux](#on-linux).
needs a Mac; see [On Linux](#on-linux). Off macOS, stim-server also refuses a
device-host request at hello and `stim-server devices grant <id> --device-host`.

Running setup on the worker is the worker-side approval. It pre-approves at
most one request per chosen capability, from this node, carrying this ticket,
Expand Down
8 changes: 7 additions & 1 deletion packages/server/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,13 @@ describe.each(['build', 'device-host'] as const)('%s approval', (capability) =>
expect(plain('devices').stdout).toContain(`${id} Laptop pending ${capability} from laptop (u)`);
expect(plain('devices', 'grant', id, '--control').stderr).toContain(`takes only --${capability}`);
expect(plain('devices', 'grant', id, `--${capability}`, '--read').status).toBe(1);
expect(plain('devices', 'grant', id, `--${capability}`).status).toBe(0);
const grants = capability === 'build' || process.platform === 'darwin';
const granted = plain('devices', 'grant', id, `--${capability}`);
expect({ status: granted.status, refused: granted.stderr.includes('Hosting simulators needs a Mac') }).toEqual({
status: grants ? 0 : 1,
refused: !grants,
});
if (!grants) return;
const { devices } = run('devices', '--json') as { devices: Record<string, unknown>[] };
expect(devices).toEqual([
expect.objectContaining({ id, capabilities: [capability], requestedCapability: capability }),
Expand Down
22 changes: 22 additions & 0 deletions packages/server/__tests__/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ async function start(
overrides: {
host?: ServerOptions['host'];
loopbackOnly?: boolean;
hostsDevices?: boolean;
exit?: boolean;
whoisDelayMs?: number;
authTimeoutMs?: number;
Expand Down Expand Up @@ -365,6 +366,7 @@ async function start(
chmodSync(tailscale, 0o755);
server = await startServer({
host: overrides.host,
hostsDevices: overrides.hostsDevices ?? true,
hosts: ['127.0.0.1'],
port: 0,
stimCli,
Expand Down Expand Up @@ -1654,6 +1656,26 @@ describe('offloaded builds', () => {
}
}

test.skipIf(!fakeTailscale)(
'a machine that cannot host refuses device-host requests and still takes build requests',
async () => {
const port = await start({ hostsDevices: false });
const asking = await connect(port, '100.64.0.3');
expect(
await asking.request('hello', {
protocol: 1,
client: CLIENT,
auth: { request: 'device-host', deviceName: 'Host client' },
}),
).toMatchObject({ error: { code: 'forbidden', message: expect.stringContaining('needs a Mac') } });
expect(readDeviceHostClients()).toEqual([]);
const { client } = await buildClient(port);
expect(await client.request('build.offer', { repo: 'app-1' })).toMatchObject({
result: { capacity: { max: 1 } },
});
},
);

test.skipIf(!fakeTailscale)(
'mirrors a content-addressed manifest, builds it and returns the artifact with its digest',
async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/server/__tests__/setup-endpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ afterEach(async () => {

async function start(wait = true): Promise<string> {
server = await startServer({
hostsDevices: true,
hosts: ['127.0.0.1'],
port: 0,
stimCli: join(home, 'unused.mjs'),
Expand Down
2 changes: 2 additions & 0 deletions packages/server/bin/stim-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ async function main(): Promise<void> {
if (grant && arg !== undefined && rest.length === 0) {
if (scope === null || scope === 'many')
fail('devices grant takes exactly one of --control, --read, --build or --device-host.');
if (scope === 'device-host' && process.platform !== 'darwin')
fail('Hosting simulators needs a Mac. This machine takes build requests only; approve them with --build.');
const capabilities = scope === 'build' || scope === 'device-host' ? [scope] : capabilitiesFor(scope === 'control');
const outcome = grantDevice(arg, [...capabilities]);
if (outcome === 'unknown') {
Expand Down
10 changes: 10 additions & 0 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ export interface ServerOptions {
* such as one forwarded by `tailscale serve` to a loopback port, so only this machine's own connections are served.
*/
loopbackOnly?: boolean;
/** Whether this machine can host simulators and apps for other machines; only a Mac can. Defaults to running on macOS. */
hostsDevices?: boolean;
port: number;
stimCli: string;
stimVersion: string;
Expand Down Expand Up @@ -1136,6 +1138,14 @@ export async function startServer(options: ServerOptions): Promise<RunningServer
CLOSE_UNAUTHORIZED,
);
}
if (auth.request === 'device-host' && !(options.hostsDevices ?? process.platform === 'darwin')) {
return refuse(
id,
'forbidden',
'Hosting simulators needs a Mac. This machine takes build requests only.',
CLOSE_UNAUTHORIZED,
);
}
outcome =
auth.request === 'build'
? requestBuildAccess(deviceName, identity, undefined, auth.setupTicket)
Expand Down
Loading