@intentic/sandbox-contract 1.209.0 → 1.210.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/host.contract.d.ts +1 -0
- package/dist/contracts/host.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +1 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/conversation-ids.d.ts.map +1 -1
- package/dist/conversation-ids.js +13 -3
- package/dist/conversation-ids.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/schemas.d.ts +4 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +2 -2
- package/dist/schemas.js.map +1 -1
- package/package.json +4 -4
- package/src/conversation-ids.test.ts +37 -1
- package/src/conversation-ids.ts +18 -5
- package/src/schemas.ts +19 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/sandbox-contract",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.210.0",
|
|
4
4
|
"description": "oRPC wire contract for the intentic sandbox daemon — shared by the daemon and its browser client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -89,9 +89,9 @@
|
|
|
89
89
|
"@orpc/contract": "1.14.13",
|
|
90
90
|
"tslib": "2.8.1",
|
|
91
91
|
"zod": "4.4.3",
|
|
92
|
-
"@intentic/constants": "1.
|
|
93
|
-
"@intentic/extension-manifest": "1.
|
|
94
|
-
"@intentic/registry": "1.
|
|
92
|
+
"@intentic/constants": "1.210.0",
|
|
93
|
+
"@intentic/extension-manifest": "1.210.0",
|
|
94
|
+
"@intentic/registry": "1.210.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
97
|
"@types/node": "24.13.2",
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
import { expect, test } from "vitest";
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
2
2
|
import { newConversationId } from "./conversation-ids.js";
|
|
3
3
|
import { ConversationIdSchema } from "./schemas.js";
|
|
4
4
|
|
|
5
|
+
const mockRandomValues = (values: readonly number[]) => {
|
|
6
|
+
const remaining = [...values];
|
|
7
|
+
const getRandomValues = vi.spyOn(crypto, "getRandomValues").mockImplementation(<T extends ArrayBufferView | null>(array: T): T => {
|
|
8
|
+
if (!(array instanceof Uint32Array)) {
|
|
9
|
+
throw new TypeError(`Expected a Uint32Array`);
|
|
10
|
+
}
|
|
11
|
+
const value = remaining.shift();
|
|
12
|
+
if (value === undefined) {
|
|
13
|
+
throw new Error(`No mocked random value remains`);
|
|
14
|
+
}
|
|
15
|
+
array[0] = value;
|
|
16
|
+
return array;
|
|
17
|
+
});
|
|
18
|
+
return { getRandomValues, remaining };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
afterEach(() => vi.restoreAllMocks());
|
|
22
|
+
|
|
5
23
|
// The one property that is not a matter of taste: this string becomes a git branch and a filesystem path, and
|
|
6
24
|
// the id guard is what stands between those and an injection. Held over a large sample rather than one draw,
|
|
7
25
|
// because the generator picks from three independent spaces and any of them could produce the bad character.
|
|
@@ -18,6 +36,24 @@ test("an id reads as a word pair with a short tail, and stays short", () => {
|
|
|
18
36
|
expect(id.length).toBeLessThan(24);
|
|
19
37
|
});
|
|
20
38
|
|
|
39
|
+
test("retries a random draw outside the last complete bucket", () => {
|
|
40
|
+
const bucketSize = Math.floor(2 ** 32 / 36);
|
|
41
|
+
const { getRandomValues, remaining } = mockRandomValues([0, 0, 0xffff_ffff, 0, bucketSize, bucketSize * 2, bucketSize * 3]);
|
|
42
|
+
|
|
43
|
+
expect(newConversationId()).toBe(`amber-alder-0123`);
|
|
44
|
+
expect(getRandomValues).toHaveBeenCalledTimes(7);
|
|
45
|
+
expect(remaining).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("maps complete random buckets to equal-width base36 characters", () => {
|
|
49
|
+
const bucketSize = Math.floor(2 ** 32 / 36);
|
|
50
|
+
const limit = bucketSize * 36;
|
|
51
|
+
const { remaining } = mockRandomValues([0, 0, bucketSize - 1, bucketSize, bucketSize * 35, limit - 1]);
|
|
52
|
+
|
|
53
|
+
expect(newConversationId()).toBe(`amber-alder-01zz`);
|
|
54
|
+
expect(remaining).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
21
57
|
// The tail is what makes the readable half safe to repeat: names may rhyme, ids may not.
|
|
22
58
|
test("ids are unique across a burst", () => {
|
|
23
59
|
const ids = new Set(Array.from({ length: 5_000 }, newConversationId));
|
package/src/conversation-ids.ts
CHANGED
|
@@ -146,14 +146,27 @@ const NOUNS = [
|
|
|
146
146
|
// collision far past the life of any workspace, and is still short enough to be ignored while reading.
|
|
147
147
|
const TAIL_LENGTH = 4;
|
|
148
148
|
|
|
149
|
+
// A Uint32Array draw has 2^32 possible values. Reject the short remainder above the last full bucket before
|
|
150
|
+
// dividing it into `upperBound` equal ranges; folding that remainder back with `%` makes its first few answers
|
|
151
|
+
// slightly more likely, which is exactly the bias a CSPRNG is meant to avoid.
|
|
152
|
+
const UINT32_RANGE = 0x1_0000_0000;
|
|
153
|
+
const randomBelow = (upperBound: number): number => {
|
|
154
|
+
const bucketSize = Math.floor(UINT32_RANGE / upperBound);
|
|
155
|
+
const limit = bucketSize * upperBound;
|
|
156
|
+
while (true) {
|
|
157
|
+
const value = crypto.getRandomValues(new Uint32Array(1))[0]!;
|
|
158
|
+
if (value < limit) {
|
|
159
|
+
return Math.floor(value / bucketSize);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
149
164
|
// Uniform over the array, drawn from the platform CSPRNG — `Math.random()` is seeded per process, and two
|
|
150
165
|
// browser tabs opened in the same instant are exactly the case this must not produce the same name for.
|
|
151
|
-
const pick = <T>(values: readonly T[]): T => values[
|
|
152
|
-
|
|
153
|
-
const randomInts = (count: number): Uint32Array => crypto.getRandomValues(new Uint32Array(count));
|
|
166
|
+
const pick = <T>(values: readonly T[]): T => values[randomBelow(values.length)]!;
|
|
154
167
|
|
|
155
|
-
// Lowercase base36, one character per
|
|
156
|
-
const tail = (): string => Array.from(
|
|
168
|
+
// Lowercase base36, one character per draw — 0-9a-z, all of which the id guard accepts.
|
|
169
|
+
const tail = (): string => Array.from({ length: TAIL_LENGTH }, () => randomBelow(36).toString(36)).join("");
|
|
157
170
|
|
|
158
171
|
/* A fresh conversation id: `<adjective>-<noun>-<tail>`, e.g. `swift-otter-k9m2`. Sixteen characters or so
|
|
159
172
|
* against a UUID's thirty-six, and the first eleven of them are the ones a person reads. */
|
package/src/schemas.ts
CHANGED
|
@@ -5379,14 +5379,19 @@ export type MachineSandbox = z.infer<typeof MachineSandboxSchema>;
|
|
|
5379
5379
|
/* ONE OPERATION ON ONE SANDBOX ON ONE MACHINE — the Computers view's buttons, and the only thing that changes a
|
|
5380
5380
|
* machine's fleet from a browser.
|
|
5381
5381
|
*
|
|
5382
|
-
* All
|
|
5383
|
-
* behave underneath: three are a docker call that returns in a second, three run the `ic` flow for minutes,
|
|
5384
|
-
* one
|
|
5385
|
-
* render. So every op answers as a STREAM of lines ending in a result — the fast ones simply
|
|
5386
|
-
*
|
|
5387
|
-
*
|
|
5388
|
-
*
|
|
5389
|
-
|
|
5382
|
+
* All eight ops travel one route because they are one decision to the person clicking, however differently they
|
|
5383
|
+
* behave underneath: three are a docker call that returns in a second, three run the `ic` flow for minutes, one
|
|
5384
|
+
* deletes, and one only reads. Splitting them by duration would put the same button on two doors and give the
|
|
5385
|
+
* view two shapes to render. So every op answers as a STREAM of lines ending in a result — the fast ones simply
|
|
5386
|
+
* have little to say, and `logs` is the case where the lines ARE the answer.
|
|
5387
|
+
*
|
|
5388
|
+
* `logs` is here rather than on a route of its own for the same reason: it is a button in the same row as the
|
|
5389
|
+
* other seven, on a container that may be too broken to answer any other way, and the stream shape already
|
|
5390
|
+
* carries "many lines, then an outcome" exactly as a log tail wants to arrive.
|
|
5391
|
+
*
|
|
5392
|
+
* The machine enforces which of them it will do: `sandboxes` covers the first six and the log tail, removal takes
|
|
5393
|
+
* its own switch, and a refusal comes back as the machine's own sentence naming the control to flip. */
|
|
5394
|
+
export const MachineSandboxOpSchema = z.enum(["start", "stop", "restart", "update", "rebuild", "rollback", "remove", "logs"]);
|
|
5390
5395
|
export type MachineSandboxOp = z.infer<typeof MachineSandboxOpSchema>;
|
|
5391
5396
|
|
|
5392
5397
|
export const MachineSandboxFlowSchema = z.object({
|
|
@@ -5554,8 +5559,12 @@ export const ComputersListSchema = z.object({ computers: z.array(ComputerSchema)
|
|
|
5554
5559
|
// routinely newer than the daemon it is pointed at during a rolling update.
|
|
5555
5560
|
export const SyncStatusSchema = z.object({
|
|
5556
5561
|
enrolled: z.boolean(),
|
|
5557
|
-
|
|
5558
|
-
|
|
5562
|
+
/* Whether this sandbox can do desktop sync at all. It used to be the SSH hostname the laptop would dial, and
|
|
5563
|
+
* its absence meant "this sandbox's reachability can't carry SSH" — true of every sandbox on the platform's
|
|
5564
|
+
* own fabric, which is what made sync fail on the default path. The transport rides the daemon's own HTTPS
|
|
5565
|
+
* surface now, so a sandbox that can answer this read can also sync. Kept as a field rather than assumed,
|
|
5566
|
+
* because the card branches on it and a daemon too old to say is one that should not be offered sync. */
|
|
5567
|
+
available: z.boolean().optional(),
|
|
5559
5568
|
// The single machine holding file sync, and when its heartbeat last landed.
|
|
5560
5569
|
syncingFrom: z.string().optional(),
|
|
5561
5570
|
syncSeenAt: z.number().optional(),
|