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: 3 additions & 0 deletions src/contract/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ Three identities:
- OS constraint: a bare version is exact (string equality). A range is one or more of
`>=`, `>`, `<=`, `<` + a dotted version, joined by single spaces, or `A - B`.
`^`, `~`, `*`, `||` and `x` read as a range and are refused.
- `lease.request` holds an exact `osVersion` to `componentVersionSchema`'s bound (1 to 64
characters, no whitespace, control character or `;`, no leading `-`): a downloadable missing
runtime installs it as a component.
- A partial version covers its prefix: `<=26` takes every 26.x, `>26` excludes it. A version that
is not dotted integers (`Baklava`) matches only exactly.
- Duration: `500` (ms), `2s`, `3m`, `1h`, `7d`. `undefined` when not one or not a safe integer.
Expand Down
43 changes: 37 additions & 6 deletions src/contract/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ describe("component.install input", () => {
["a newline", "35\n"],
["a control character", "35\u0000"],
["starting with '-', like an option", "-35"],
["carrying ';', sdkmanager's package path separator", "35;extras"],
])("refuses a version that is %s", (_label, version) => {
expect(parse(version)).toBe(false);
});
Expand All @@ -261,12 +262,20 @@ describe("lease.request osVersion", () => {
platform: "ios",
});

it.each([["18.4"], ["Baklava"], ["34-ext12"], [">=18"], [">=18 <26"], ["18 - 26"]])(
"accepts %s",
(osVersion) => {
expect(parse(osVersion).success).toBe(true);
},
);
it.each([
["18.4"],
["17.4"],
["34"],
["18.5.1"],
["Baklava"],
["34-ext12"],
["x".repeat(64)],
[">=18"],
[">=18 <26"],
["18 - 26"],
])("accepts %s", (osVersion) => {
expect(parse(osVersion).success).toBe(true);
});

it.each([["^18"], ["~18"], ["18.x"], ["18.X"], ["*"], [">=18 || <17"], [""]])(
"refuses %j with a message naming the accepted forms",
Expand All @@ -279,6 +288,28 @@ describe("lease.request osVersion", () => {
}
},
);

it.each([
["trailing text after a space", "17.0 -foo"],
["a leading '-', like an option", "-1"],
["a ';', sdkmanager's package path separator", "34;extras"],
["more than 64 characters", "x".repeat(65)],
["a control character", "17.4\u0007"],
["a leading space", " 17.4"],
])(
"refuses an exact version with %s, naming the bound a component version has",
(_label, osVersion) => {
const result = parse(osVersion);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues).toHaveLength(1);
expect(result.error.issues[0]?.message).toBe(
"an exact OS version is 1 to 64 characters, none of them whitespace, a control character or `;`, and does not start with `-`",
);
expect(result.error.issues[0]?.path).toEqual(["osVersion"]);
}
},
);
});

describe("lease.request leaseId", () => {
Expand Down
41 changes: 27 additions & 14 deletions src/contract/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ export const LEASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;

export const leaseIdSchema = z.string().regex(LEASE_ID_PATTERN);

/**
* why: a component's version (`26.4`, `35`) reaches the driver unread and becomes an installer
* argument, so its shape is bounded here (safety rule 10): no whitespace or control character, no
* `;` (sdkmanager's package path separator), no leading `-` (an option). An exact `osVersion` in
* `lease.request` is held to it too, because a downloadable missing runtime installs that string.
*/
const componentVersionSchema = z
.string()
.min(1)
.max(64)
.regex(/^[^\s\p{Cc};]+$/u, "must contain no whitespace, control character or ';'")
.refine((version) => !version.startsWith("-"), "must not start with '-'");

const EXACT_OS_VERSION_BOUND =
"an exact OS version is 1 to 64 characters, none of them whitespace, a control character or `;`, and does not start with `-`";

const leaseRequestBaseSchema = z
.object({
model: z.string().min(1).optional(),
Expand Down Expand Up @@ -281,6 +297,17 @@ function refuseBadOsVersion(): <T extends { readonly osVersion?: string | undefi
message: parsed.message,
path: ["osVersion"],
});
return;
}
if (
parsed.constraint.kind === "exact" &&
!componentVersionSchema.safeParse(parsed.constraint.version).success
) {
context.addIssue({
code: "custom",
message: EXACT_OS_VERSION_BOUND,
path: ["osVersion"],
});
}
};
}
Expand Down Expand Up @@ -860,20 +887,6 @@ export const workerUpdate = defineOperation({

// ---- component.install (ADR 0010 §6) --------------------------------------------------------

/**
* A component's version as the catalog lists it under `runtimes` (`26.4`, `35`), carried to the
* driver unread -- which is why its shape is bounded here, before it reaches one (safety rule
* 10): a driver builds an installer argument from it. One to 64 characters, none of them
* whitespace or a control character, and not starting with `-`, which an installer could read
* as an option.
*/
const componentVersionSchema = z
.string()
.min(1)
.max(64)
.regex(/^[^\s\p{Cc}]+$/u, "must contain no whitespace or control character")
.refine((version) => !version.startsWith("-"), "must not start with '-'");

/**
* Installs one iOS simulator runtime or Android system image without leasing a device (ADR 0010
* §6). Admin-only: the command is an operator's consent to a multi-GB download, which
Expand Down
2 changes: 1 addition & 1 deletion src/daemon/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Its stdout lines move the state; a failed line, a non-zero exit or a failed laun
2. **Hello** — first frame must be `hello`, else `HANDSHAKE_REQUIRED` and close.
Role: the uplink connection is `admin` by acceptance; otherwise the resolver. Principal: `hello.principal`, else `SIMLOCK_AGENT_ID`, else the daemon's pid. Both are fixed for the connection's life.
3. **Version** — ranges overlap → highest common version. `PROTOCOL_VERSION_RANGE`
is `{ min: 59, max: 59 }` (`src/contract/protocol.ts`). No overlap → the
is `{ min: 62, max: 62 }` (`src/contract/protocol.ts`). No overlap → the
connection stays open, and every request but `daemon.stop` gets
`PROTOCOL_VERSION_UNSUPPORTED`.
4. **Request** — one frame → one `dispatch()` call. While stopping: `DAEMON_STOPPING`.
Expand Down
14 changes: 11 additions & 3 deletions src/daemon/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2410,7 +2410,16 @@ describe("DaemonServer download policy", () => {

it("still answers a lease request whose download names a component past the progress push's bound, sending no downloading push for it and logging that", async () => {
const clock = new FakeClock(1_000);
const driver = new FakeDriver({ availableOsVersions: [], clock, platform: "ios" });
const driver = new FakeDriver({ availableOsVersions: ["26.5"], clock, platform: "ios" });
// why: the driver, not the request, names the component: a request's exact osVersion is held
// within the push's bound. Every resolve before the install answers as a missing runtime does.
for (const callNumber of [1, 2]) {
driver.failOn(
"resolveSpec",
callNumber,
new RuntimeMissingError("ios", "26.5", { component: "9".repeat(65) }),
);
}
const sink = new MemoryLogSink();
const harness = await createHarness({
clock,
Expand All @@ -2419,13 +2428,12 @@ describe("DaemonServer download policy", () => {
});
const client = await createClient(harness.socketPath);
await hello(client);
const osVersion = "9".repeat(65);

const grant = await client.request("lease.request", {
allowDownload: true,
requesterId: "agent-1",
model: "iPhone 16",
osVersion,
osVersion: "26.5",
platform: "ios",
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Without `--os`, the lease gets the newest runtime installed for that platform. U

A range is one or more of `>=`, `>`, `<=`, `<` followed by a version, separated by single spaces, or a hyphen range with spaces around the hyphen. A short version covers everything under it, so `<=26` includes 26.1. Quote ranges so the shell does not read `>` or `<`.

Forms like `^18`, `~18`, `18.x`, `*` and `||` are refused with `BAD_REQUEST` (exit 2). Any other text is an exact version, which is how Android names such as `Baklava` or `34-ext12` work.
Forms like `^18`, `~18`, `18.x`, `*` and `||` are refused with `BAD_REQUEST` (exit 2). Any other text is an exact version, which is how Android names such as `Baklava` or `34-ext12` work. An exact version is 1 to 64 characters with no spaces, control characters or `;`, and does not start with `-`; anything else is refused with `BAD_REQUEST`.

A range takes an idle device whose OS is inside it first, and otherwise creates a device on the newest installed runtime inside it. A range never downloads anything: if no installed runtime is inside it, the lease fails at once with `RUNTIME_MISSING` (exit 12).

Expand Down
Loading