@intentic/sandbox-contract 1.224.0 → 1.225.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/README.md +14 -14
- package/dist/agent-catalog.js +3 -3
- package/dist/agent-catalog.js.map +1 -1
- package/dist/chores/chores.js +42 -42
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/extension-update.js +2 -2
- package/dist/chores/extension-update.js.map +1 -1
- package/dist/chores/fix-deps.js +1 -1
- package/dist/chores/fix-deps.js.map +1 -1
- package/dist/chores/probes.js +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/chores/verdict.js +2 -2
- package/dist/chores/verdict.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +41 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/exit.contract.d.ts +80 -0
- package/dist/contracts/exit.contract.d.ts.map +1 -0
- package/dist/contracts/exit.contract.js +13 -0
- package/dist/contracts/exit.contract.js.map +1 -0
- package/dist/contracts/settings.contract.d.ts +12 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/events.d.ts +4 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -5
- package/dist/events.js.map +1 -1
- package/dist/fast-tier.d.ts +9 -0
- package/dist/fast-tier.d.ts.map +1 -0
- package/dist/fast-tier.js +19 -0
- package/dist/fast-tier.js.map +1 -0
- package/dist/history-state.js +3 -3
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +203 -68
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/model-order.d.ts +1 -0
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +5 -0
- package/dist/model-order.js.map +1 -1
- package/dist/output-fields.d.ts.map +1 -1
- package/dist/output-fields.js +2 -2
- package/dist/output-fields.js.map +1 -1
- package/dist/prompt-complexity.d.ts +19 -0
- package/dist/prompt-complexity.d.ts.map +1 -0
- package/dist/prompt-complexity.js +83 -0
- package/dist/prompt-complexity.js.map +1 -0
- package/dist/publish-drafts.js +2 -2
- package/dist/publish-drafts.js.map +1 -1
- package/dist/schemas.d.ts +214 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +113 -2
- package/dist/schemas.js.map +1 -1
- package/dist/workflow-faults.js +3 -3
- package/dist/workflow-faults.js.map +1 -1
- package/dist/workspace-state.d.ts +20 -20
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +20 -20
- package/dist/workspace-state.js.map +1 -1
- package/package.json +5 -5
- package/src/agent-catalog.test.ts +25 -25
- package/src/agent-catalog.ts +3 -3
- package/src/agent-run-model.test.ts +3 -3
- package/src/capability-ledger.test.ts +13 -13
- package/src/chores/chores.test.ts +1 -1
- package/src/chores/chores.ts +42 -42
- package/src/chores/digest.test.ts +1 -1
- package/src/chores/extension-update.ts +2 -2
- package/src/chores/fix-deps.ts +1 -1
- package/src/chores/probes.test.ts +6 -6
- package/src/chores/probes.ts +1 -1
- package/src/chores/stack.test.ts +3 -3
- package/src/chores/verdict.test.ts +20 -20
- package/src/chores/verdict.ts +2 -2
- package/src/contract-lock.test.ts +1 -1
- package/src/contracts/exit.contract.ts +42 -0
- package/src/contracts/providers.contract.ts +1 -1
- package/src/conversation-ids.ts +1 -1
- package/src/events.test.ts +3 -3
- package/src/events.ts +6 -6
- package/src/fast-tier.test.ts +88 -0
- package/src/fast-tier.ts +72 -0
- package/src/history-state.ts +3 -3
- package/src/hostnames.test.ts +1 -1
- package/src/index.ts +5 -0
- package/src/model-order.test.ts +11 -11
- package/src/model-order.ts +22 -0
- package/src/output-fields.ts +2 -2
- package/src/path-refs.test.ts +4 -4
- package/src/prompt-complexity.test.ts +160 -0
- package/src/prompt-complexity.ts +271 -0
- package/src/publish-drafts.ts +2 -2
- package/src/quick-model.test.ts +11 -11
- package/src/routes.test.ts +11 -5
- package/src/runtime-state.test.ts +1 -1
- package/src/schemas.test.ts +8 -8
- package/src/schemas.ts +311 -6
- package/src/search-globs.test.ts +2 -2
- package/src/share-paths.test.ts +1 -1
- package/src/title.test.ts +9 -9
- package/src/title.ts +1 -1
- package/src/tunnel-ids.test.ts +3 -3
- package/src/versions.test.ts +3 -3
- package/src/versions.ts +1 -1
- package/src/workflow-faults.test.ts +6 -6
- package/src/workflow-faults.ts +3 -3
- package/src/workspace-state.test.ts +32 -32
- package/src/workspace-state.ts +20 -20
package/src/schemas.test.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { CapabilitiesListSchema, IpsecVpnConfigSchema, SandboxSettingsSchema } f
|
|
|
3
3
|
|
|
4
4
|
/* The settings shape spans a version seam that really moves: the browser ships with the platform, the daemon
|
|
5
5
|
* ships inside the user's sandbox image, so a web build routinely parses a payload from an OLDER daemon. These
|
|
6
|
-
* tests pin the property that makes that survivable
|
|
7
|
-
* failure
|
|
6
|
+
* tests pin the property that makes that survivable: an absent key is that flag's default, not a parse
|
|
7
|
+
* failure, because failing instead reaches the user as a settings page whose switches silently do nothing. */
|
|
8
8
|
|
|
9
9
|
test("a payload from a build that predates a toggle parses, with the new toggle at its default", () => {
|
|
10
10
|
// What a daemon built before the output-cleaner backend switch answers with: every key it knew, and
|
|
@@ -19,25 +19,25 @@ test("a payload from a build that predates a toggle parses, with the new toggle
|
|
|
19
19
|
outputHoldout: 0.1,
|
|
20
20
|
};
|
|
21
21
|
// The defaults come from the schema, not from a copy of it written here. Transcribing them made every
|
|
22
|
-
// setting the product gained land as a failure in this file
|
|
22
|
+
// setting the product gained land as a failure in this file: a diff that only ever said "the list moved",
|
|
23
23
|
// never "tolerance broke", and whose fix was always to paste the new default in. What this test is about
|
|
24
24
|
// is the seam: what the old build sent survives verbatim, and what it never heard of arrives at default.
|
|
25
25
|
expect(SandboxSettingsSchema.parse(older)).toEqual({ ...SandboxSettingsSchema.parse({}), ...older });
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
/* The invariant a fresh sandbox depends on: NO field is required. A settings object is written for the first
|
|
29
|
-
* time only when the user changes something, so until then the daemon parses `{}
|
|
29
|
+
* time only when the user changes something, so until then the daemon parses `{}`: one field without a
|
|
30
30
|
* `.default()` turns that into a throw at boot, and the version tolerance above is built on the same property.
|
|
31
31
|
*
|
|
32
32
|
* Asserted by shape rather than by value: what each default IS belongs next to the field in schemas.ts, where
|
|
33
33
|
* the reason it holds is written down. A second copy here proved nothing the schema didn't already say and
|
|
34
34
|
* failed on every field the product added. */
|
|
35
|
-
test("no field is required
|
|
35
|
+
test("no field is required: a workspace that has never written settings parses", () => {
|
|
36
36
|
const defaults = SandboxSettingsSchema.parse({});
|
|
37
37
|
expect(Object.keys(defaults).toSorted()).toEqual(Object.keys(SandboxSettingsSchema.shape).toSorted());
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
test("a key of the wrong type is still a parse failure
|
|
40
|
+
test("a key of the wrong type is still a parse failure: tolerance is for absence, not for garbage", () => {
|
|
41
41
|
expect(SandboxSettingsSchema.safeParse({ iqSearch: "yes" }).success).toBe(false);
|
|
42
42
|
expect(SandboxSettingsSchema.safeParse({ outputHoldout: 4 }).success).toBe(false);
|
|
43
43
|
// The prompt cap is a real bound, not advice: the text IS the system prompt, and every turn pays for it.
|
|
@@ -46,7 +46,7 @@ test("a key of the wrong type is still a parse failure — tolerance is for abse
|
|
|
46
46
|
|
|
47
47
|
/* The capability list crosses the same seam, and its failure mode is worse than a dead switch: the browser
|
|
48
48
|
* parses ONE object for the whole page, so a required key the older daemon never sends takes the Capabilities
|
|
49
|
-
* page down entirely
|
|
49
|
+
* page down entirely: to hide an advisory badge. */
|
|
50
50
|
|
|
51
51
|
test("a capability list from a daemon that predates recommendations parses, with none recommended", () => {
|
|
52
52
|
const older = { capabilities: [{ id: "github", kind: "cli", status: { state: "active" }, config: { provider: "github" } }] };
|
|
@@ -66,7 +66,7 @@ test("an ipsec tunnel is a full tunnel unless it says otherwise", () => {
|
|
|
66
66
|
test("routed networks take a CIDR list and reject what charon could not load", () => {
|
|
67
67
|
expect(IpsecVpnConfigSchema.parse({ ...ipsec, routedNetworks: "10.0.0.0/8, 192.168.0.0/16" }).routedNetworks).toBe("10.0.0.0/8, 192.168.0.0/16");
|
|
68
68
|
expect(IpsecVpnConfigSchema.parse({ ...ipsec, routedNetworks: "fd00::/8" }).routedNetworks).toBe("fd00::/8");
|
|
69
|
-
// A bare host address is the easy mistake
|
|
69
|
+
// A bare host address is the easy mistake: strongSwan wants the prefix, and the message says so.
|
|
70
70
|
expect(IpsecVpnConfigSchema.safeParse({ ...ipsec, routedNetworks: "192.168.0.168" }).success).toBe(false);
|
|
71
71
|
expect(IpsecVpnConfigSchema.safeParse({ ...ipsec, routedNetworks: "10.0.0.0/8,nonsense" }).success).toBe(false);
|
|
72
72
|
expect(IpsecVpnConfigSchema.safeParse({ ...ipsec, routedNetworks: "" }).success).toBe(false);
|
package/src/schemas.ts
CHANGED
|
@@ -1873,6 +1873,39 @@ export const SandboxSettingsSchema = z.object({
|
|
|
1873
1873
|
* surface MAY still name one (the shared run button's caret, Acceptance's per-run pick), and that wins. */
|
|
1874
1874
|
agentRunModels: z.array(z.string()).max(10).default([]),
|
|
1875
1875
|
agentRunEffort: z.string().default(""),
|
|
1876
|
+
/* AUTOMATIC TIER SELECTION: may the daemon run an easy-looking turn on a cheaper rung of the provider the
|
|
1877
|
+
* user is already on, instead of on the model they picked?
|
|
1878
|
+
*
|
|
1879
|
+
* THREE STATES RATHER THAN A TOGGLE, because the middle one is the only honest way to reach the third.
|
|
1880
|
+
* Nobody, this repo included, can name a sensible cutoff for "easy enough" without traffic to fit it
|
|
1881
|
+
* against, and a routing threshold guessed in advance is how a cost feature quietly becomes a quality
|
|
1882
|
+
* regression. So:
|
|
1883
|
+
* off — the judge never runs. Nothing is scored, nothing is recorded, turns run on the user's pick.
|
|
1884
|
+
* shadow — the judge runs and its verdict is written to the spend ledger beside what the turn actually
|
|
1885
|
+
* cost, and NOTHING IS ROUTED. This is the default: it spends no tokens, changes no behaviour,
|
|
1886
|
+
* and is the only thing that can turn the weights in prompt-complexity.ts from a hypothesis
|
|
1887
|
+
* into a measurement.
|
|
1888
|
+
* on — a turn judged fast runs on the cheap rung (fast-tier.ts), when the provider publishes one.
|
|
1889
|
+
*
|
|
1890
|
+
* IT CAN ONLY EVER ROUTE DOWN. There is no "which model is the standard tier" setting because the standard
|
|
1891
|
+
* tier is the model the user already chose, so the worst case of a wrong verdict is one turn's quality on a
|
|
1892
|
+
* model they can see on the card and correct, never a bill they did not ask for. That asymmetry is why this
|
|
1893
|
+
* can default to shadow rather than to off: shadow costs nothing and `on` cannot overspend. */
|
|
1894
|
+
autoTier: z.enum(["off", "shadow", "on"]).default("shadow"),
|
|
1895
|
+
/* WHICH CHEAP MODEL A DOWNGRADED TURN LANDS ON, an ordered list of `${provider}:${model}` keys
|
|
1896
|
+
* (quickModelKey), or EMPTY for Auto.
|
|
1897
|
+
*
|
|
1898
|
+
* Empty is the default and the interesting case, exactly as quickModel's is: Auto is the cheapest row the
|
|
1899
|
+
* turn's own provider publishes, read through the same cheap-end order (compareCheapestFirst), so the two
|
|
1900
|
+
* features can never disagree about which rung is the cheap one, and connecting an account tomorrow
|
|
1901
|
+
* improves the answer by itself.
|
|
1902
|
+
*
|
|
1903
|
+
* A LIST, so a sandbox working across several providers can name the rung it wants on each. But unlike the
|
|
1904
|
+
* two lists above this one is NOT a failure ladder: entries naming a provider other than the turn's own are
|
|
1905
|
+
* dropped rather than tried, because switching provider retires the conversation's session (turnRequest.ts
|
|
1906
|
+
* `resumes`), and starting the conversation over to save a fraction of a cent is not a saving. The first
|
|
1907
|
+
* entry that names this provider AND is genuinely cheaper than the pick wins; if none does, Auto answers. */
|
|
1908
|
+
autoFastModels: z.array(z.string()).max(10).default([]),
|
|
1876
1909
|
// How long a finished agent stays on the board before it is archived automatically (days; 0 ⇒ never).
|
|
1877
1910
|
// Unlike every other flag here this one defaults ON, because the lane it governs is the board's only
|
|
1878
1911
|
// terminal state: without a sweep the Finished lane grows for the life of the sandbox, and each card it
|
|
@@ -3192,12 +3225,14 @@ export const CapabilityKindSchema = z.enum([
|
|
|
3192
3225
|
"extension",
|
|
3193
3226
|
"ssh",
|
|
3194
3227
|
"vpn",
|
|
3228
|
+
"exit",
|
|
3195
3229
|
"docker",
|
|
3196
3230
|
"browser",
|
|
3197
3231
|
"identity",
|
|
3198
3232
|
"host",
|
|
3199
3233
|
"agent",
|
|
3200
3234
|
"endpoint",
|
|
3235
|
+
"localmodel",
|
|
3201
3236
|
"wallet",
|
|
3202
3237
|
]);
|
|
3203
3238
|
export type CapabilityKind = z.infer<typeof CapabilityKindSchema>;
|
|
@@ -3316,7 +3351,7 @@ export const isForticlientCiphertext = (value: string): boolean => /^Enc[X]?\s+[
|
|
|
3316
3351
|
|
|
3317
3352
|
const notForticlientCiphertext = <T extends z.ZodType<string>>(field: T, label: string): T =>
|
|
3318
3353
|
field.refine((value) => !isForticlientCiphertext(value), {
|
|
3319
|
-
message: `That looks like a value copied straight out of a FortiClient config
|
|
3354
|
+
message: `That looks like a value copied straight out of a FortiClient config, FortiClient encrypts it with a key tied to the machine that exported it, so it can't be used here. Enter the actual ${label} (ask whoever administers the gateway).`,
|
|
3320
3355
|
}) as unknown as T;
|
|
3321
3356
|
|
|
3322
3357
|
export const WireguardVpnConfigSchema = z.object({
|
|
@@ -3393,6 +3428,75 @@ export const IpsecVpnConfigSchema = z.object({
|
|
|
3393
3428
|
autoConnect,
|
|
3394
3429
|
});
|
|
3395
3430
|
export const VpnConfigSchema = z.discriminatedUnion("provider", [WireguardVpnConfigSchema, FortinetVpnConfigSchema, IpsecVpnConfigSchema]);
|
|
3431
|
+
|
|
3432
|
+
// ---- exit ----
|
|
3433
|
+
/* A GEO EXIT: somewhere the agent's traffic can LEAVE from, so a page fetches as if read in Berlin or Osaka.
|
|
3434
|
+
* Its own kind rather than a fourth `vpn` provider, and the distinction is the whole reason this works:
|
|
3435
|
+
*
|
|
3436
|
+
* a `vpn` REACHES a private network , one stored gateway, dialled, pushing its routes into the main table.
|
|
3437
|
+
* an `exit` LEAVES from somewhere else, a POOL with a catalog, switched at runtime, routing NOTHING into
|
|
3438
|
+
* the main table.
|
|
3439
|
+
*
|
|
3440
|
+
* That last clause is load-bearing. An exit is a full tunnel by definition, and a full tunnel on the main
|
|
3441
|
+
* table swallows the sandbox's own uplink, the model endpoint and the tunnel that makes this sandbox
|
|
3442
|
+
* reachable, which reads to a user as the agent breaking mid-turn (see IpsecVpnConfigSchema.routedNetworks
|
|
3443
|
+
* for the same trap on the vpn kind). So an exit never touches the default route. It publishes a local SOCKS
|
|
3444
|
+
* proxy and callers opt in: a browser account naming it, `curl --proxy`, and nothing else. The side benefit
|
|
3445
|
+
* is trust, a volunteer relay carries only what was pointed at it, never the agent's own working traffic.
|
|
3446
|
+
*
|
|
3447
|
+
* Three providers, chosen because each is reachable with no paid account:
|
|
3448
|
+
* tor , the Tor network. ~52 exit countries, no account, no credentials, no privileges: it is a SOCKS
|
|
3449
|
+
* proxy already. Country is a torrc line, a new IP is a control-port signal. The free default.
|
|
3450
|
+
* vpngate , the University of Tsukuba's volunteer relay pool. No account; its public CSV IS the catalog,
|
|
3451
|
+
* so servers auto-fill. Overwhelmingly Japan/Korea in practice, which is the half of the map Tor
|
|
3452
|
+
* covers worst, so the two complement rather than duplicate.
|
|
3453
|
+
* wireguard, bring your own .conf files, one per country, from a provider's dashboard (Proton VPN's free
|
|
3454
|
+
* tier, Mullvad, anything). The catalog is built by parsing what was pasted.
|
|
3455
|
+
* Starting, switching country and rotating are LIVE operations (see exit.contract.ts), never config, so an
|
|
3456
|
+
* exit's real state is read off the machine. `country` is the resting preference and `autoStart` the only
|
|
3457
|
+
* other persisted intent. */
|
|
3458
|
+
export const ExitProviderSchema = z.enum(["tor", "vpngate", "wireguard"]);
|
|
3459
|
+
export type ExitProvider = z.infer<typeof ExitProviderSchema>;
|
|
3460
|
+
|
|
3461
|
+
// An ISO 3166-1 alpha-2 code, normalised up so "de", "DE" and "De" are one country rather than three. The
|
|
3462
|
+
// catalogs, the CLI and the manifest all speak this one spelling.
|
|
3463
|
+
export const CountryCodeSchema = z
|
|
3464
|
+
.string()
|
|
3465
|
+
.regex(/^[A-Za-z]{2}$/, "A country is its two-letter code, like DE, US or JP.")
|
|
3466
|
+
.transform((value) => value.toUpperCase());
|
|
3467
|
+
|
|
3468
|
+
// Exits rest DOWN by default, the opposite of a vpn's autoConnect. A vpn is dialled because something behind
|
|
3469
|
+
// it is unreachable otherwise; an exit costs volunteer bandwidth (tor, vpngate) and buys nothing until a task
|
|
3470
|
+
// actually wants a different country, so the honest default is to hold it until asked.
|
|
3471
|
+
const autoStart = z.enum(["on", "off"]).default("off");
|
|
3472
|
+
|
|
3473
|
+
export const TorExitConfigSchema = z.object({
|
|
3474
|
+
provider: z.literal("tor"),
|
|
3475
|
+
// Where to come out, when nothing has asked for somewhere else. Absent ⇒ let Tor choose, which is both
|
|
3476
|
+
// faster and kinder to the network.
|
|
3477
|
+
country: CountryCodeSchema.optional(),
|
|
3478
|
+
autoStart,
|
|
3479
|
+
});
|
|
3480
|
+
export const VpngateExitConfigSchema = z.object({
|
|
3481
|
+
provider: z.literal("vpngate"),
|
|
3482
|
+
country: CountryCodeSchema.optional(),
|
|
3483
|
+
autoStart,
|
|
3484
|
+
});
|
|
3485
|
+
export const WireguardExitConfigSchema = z.object({
|
|
3486
|
+
/* One or more WireGuard .conf files in one field, pasted back to back. One field rather than one
|
|
3487
|
+
* capability per country because they are one POOL: the whole point is switching between them under a
|
|
3488
|
+
* proxy port that never moves, and a user with five Proton free countries should not add five capabilities
|
|
3489
|
+
* to get five countries out of one account.
|
|
3490
|
+
*
|
|
3491
|
+
* Country per conf comes from an optional `# country: DE` line, else from the provider's own naming
|
|
3492
|
+
* convention in the Endpoint host (Proton's `de-free-01.protonvpn.net`, Mullvad's `de-ber-wg-001`), else
|
|
3493
|
+
* from a lookup through the tunnel once it is up. Whole thing is the secret: each conf holds a private key. */
|
|
3494
|
+
provider: z.literal("wireguard"),
|
|
3495
|
+
config: z.string().min(1),
|
|
3496
|
+
country: CountryCodeSchema.optional(),
|
|
3497
|
+
autoStart,
|
|
3498
|
+
});
|
|
3499
|
+
export const ExitConfigSchema = z.discriminatedUnion("provider", [TorExitConfigSchema, VpngateExitConfigSchema, WireguardExitConfigSchema]);
|
|
3396
3500
|
/* What is OPTIONAL about the in-sandbox Docker Engine. The engine itself takes no configuring, the capability
|
|
3397
3501
|
* either runs dockerd or it doesn't, so this holds only what a user chooses, and the bar for landing here is
|
|
3398
3502
|
* that the sandbox works without it. (`--privileged` therefore is not here and never will be: dockerd does not
|
|
@@ -3403,7 +3507,7 @@ export const VpnConfigSchema = z.discriminatedUnion("provider", [WireguardVpnCon
|
|
|
3403
3507
|
*
|
|
3404
3508
|
* IMAGE (`gpu`), rides the environment overlay. Changing it recomposes the Dockerfile, so it costs an
|
|
3405
3509
|
* owner-approved rebuild and a container recreate. Only `fragment()` may read these.
|
|
3406
|
-
* ENGINE (everything below it)
|
|
3510
|
+
* ENGINE (everything below it): /etc/docker/daemon.json, which dockerd reads at start. Changing one
|
|
3407
3511
|
* rewrites the file and restarts dockerd: no rebuild, no new image, but it DOES stop whatever containers
|
|
3408
3512
|
* the engine is running, which is why it is disclosed rather than silently applied.
|
|
3409
3513
|
*
|
|
@@ -3475,6 +3579,15 @@ export const BrowserConfigSchema = z
|
|
|
3475
3579
|
purpose: z.string().optional(),
|
|
3476
3580
|
// ISO-8601 date, stamped when the agent opens the account, absent for one connected by hand.
|
|
3477
3581
|
openedAt: z.string().optional(),
|
|
3582
|
+
/* WHERE THIS ACCOUNT BROWSES FROM: the id of an `exit` capability. Set it and every page this profile
|
|
3583
|
+
* opens comes out of that country, with the browser's clock, locale and languages set to match.
|
|
3584
|
+
*
|
|
3585
|
+
* Only meaningful on an account that owns its OWN profile. An account born from an identity shares
|
|
3586
|
+
* that identity's browser, cookies, passkeys and all, so it shares its exit too and this field is
|
|
3587
|
+
* ignored for it (see the daemon's browser/browser-exit.ts). That is not a limitation, it is the
|
|
3588
|
+
* point: one Google session appearing from Berlin in one tab and Osaka in another is a far louder
|
|
3589
|
+
* signal than any address, so the exit belongs to whatever owns the profile. */
|
|
3590
|
+
exit: z.string().optional(),
|
|
3478
3591
|
})
|
|
3479
3592
|
.catchall(z.string());
|
|
3480
3593
|
/* ONE EMAIL IDENTITY THE SANDBOX ACTS AS ONLINE, the container platform accounts are born from, and the answer
|
|
@@ -3507,6 +3620,10 @@ export const IdentityConfigSchema = z.object({
|
|
|
3507
3620
|
mailbox: z.string().optional(),
|
|
3508
3621
|
loginUrl: z.url().optional(),
|
|
3509
3622
|
openAccounts: z.enum(["on", "off"]).default("off"),
|
|
3623
|
+
/* WHERE THIS IDENTITY LIVES, the id of an `exit` capability. An identity OWNS a browser profile, and every
|
|
3624
|
+
* account born from it shares that profile, so setting it here sets it for all of them at once, which is
|
|
3625
|
+
* the only coherent place to set it: the shared thing is one browser, and one browser is in one place. */
|
|
3626
|
+
exit: z.string().optional(),
|
|
3510
3627
|
});
|
|
3511
3628
|
export type IdentityConfig = z.infer<typeof IdentityConfigSchema>;
|
|
3512
3629
|
/* A connected COMPUTER of the user's own, the inverse of `ssh`, which reaches a server the sandbox can dial.
|
|
@@ -3600,6 +3717,28 @@ export const EndpointConfigSchema = z.object({
|
|
|
3600
3717
|
apiKey: z.string().optional(),
|
|
3601
3718
|
headers: z.string().optional(),
|
|
3602
3719
|
});
|
|
3720
|
+
|
|
3721
|
+
/* A MODEL THE SANDBOX RUNS ITSELF, the managed counterpart of `endpoint`. An endpoint points at a server the
|
|
3722
|
+
* USER operates; this one names weights, and the daemon does the operating: it downloads the file into the
|
|
3723
|
+
* workspace cache, serves it with the image's bundled llama-server on a loopback port it owns, and registers
|
|
3724
|
+
* the result exactly as if the user had added an endpoint at that port. Everything downstream (the picker, the
|
|
3725
|
+
* translator, quick-model pinning) sees an `endpoint/<id>` provider and never learns the difference, which is
|
|
3726
|
+
* why there is no baseUrl here: the URL is derived from the entry's id (the daemon's endpoints/local-model.ts),
|
|
3727
|
+
* not a fact anyone typed.
|
|
3728
|
+
*
|
|
3729
|
+
* `model` is WHICH WEIGHTS, as a Hugging Face path (`owner/repo/file.gguf`, resolved to the repo's own
|
|
3730
|
+
* download), so shipping a new recommended model is a catalog-card edit, not a daemon release. The reserved
|
|
3731
|
+
* value "custom" defers to `url`, a direct GGUF link for people who know exactly what they want.
|
|
3732
|
+
*
|
|
3733
|
+
* `gpu` mirrors the docker card's option and rides the same allowlisted `--gpus=all` directive: the ASK lives
|
|
3734
|
+
* here, what became of it is SANDBOX_GPU, stamped by the runner (see the docker handler's gpuState). "on"/"off"
|
|
3735
|
+
* rather than a boolean for the manifest-wide reason DockerConfigSchema gives. */
|
|
3736
|
+
export const LocalModelConfigSchema = z.object({
|
|
3737
|
+
model: z.string().min(1),
|
|
3738
|
+
gpu: z.enum(["on", "off"]).default("off"),
|
|
3739
|
+
url: z.url().optional(),
|
|
3740
|
+
});
|
|
3741
|
+
export type LocalModelConfig = z.infer<typeof LocalModelConfigSchema>;
|
|
3603
3742
|
/* THE SANDBOX WALLET, a USDC balance the agent can spend on x402-payable endpoints, under owner policy.
|
|
3604
3743
|
*
|
|
3605
3744
|
* WHAT IS DELIBERATELY NOT HERE IS A KEY. The signing key lives with the PLATFORM (one wallet per owner,
|
|
@@ -3619,7 +3758,7 @@ export const EndpointConfigSchema = z.object({
|
|
|
3619
3758
|
* `allow`/`deny` are hostname lists (comma- or newline-separated). Empty allow = any host, each behind its
|
|
3620
3759
|
* card; deny wins over allow. One capability per sandbox (singleton card): a second balance would just be a
|
|
3621
3760
|
* second opinion about the same owner's wallet. */
|
|
3622
|
-
const usdAmount = z.string().regex(/^\d+(\.\d{1,6})?$/, "a USD amount like 0.50 (up to six decimals
|
|
3761
|
+
const usdAmount = z.string().regex(/^\d+(\.\d{1,6})?$/, "a USD amount like 0.50 (up to six decimals: USDC's own precision)");
|
|
3623
3762
|
export const WalletNetworkSchema = z.enum(["eip155:8453", "eip155:84532"]);
|
|
3624
3763
|
export type WalletNetwork = z.infer<typeof WalletNetworkSchema>;
|
|
3625
3764
|
export const WalletConfigSchema = z.object({
|
|
@@ -3650,6 +3789,10 @@ export type WireguardVpnConfig = z.infer<typeof WireguardVpnConfigSchema>;
|
|
|
3650
3789
|
export type FortinetVpnConfig = z.infer<typeof FortinetVpnConfigSchema>;
|
|
3651
3790
|
export type IpsecVpnConfig = z.infer<typeof IpsecVpnConfigSchema>;
|
|
3652
3791
|
export type VpnConfig = z.infer<typeof VpnConfigSchema>;
|
|
3792
|
+
export type TorExitConfig = z.infer<typeof TorExitConfigSchema>;
|
|
3793
|
+
export type VpngateExitConfig = z.infer<typeof VpngateExitConfigSchema>;
|
|
3794
|
+
export type WireguardExitConfig = z.infer<typeof WireguardExitConfigSchema>;
|
|
3795
|
+
export type ExitConfig = z.infer<typeof ExitConfigSchema>;
|
|
3653
3796
|
export type DockerConfig = z.infer<typeof DockerConfigSchema>;
|
|
3654
3797
|
export type BrowserConfig = z.infer<typeof BrowserConfigSchema>;
|
|
3655
3798
|
export type HostConfig = z.infer<typeof HostConfigSchema>;
|
|
@@ -3671,6 +3814,9 @@ export const CapabilitySchema = z.discriminatedUnion("kind", [
|
|
|
3671
3814
|
// No IFNAMSIZ cap on the id: the tunnel's interface name is DERIVED (see the daemon's vpn/vpn-paths.ts
|
|
3672
3815
|
// interfaceName) rather than being the id itself, so a descriptive name is free.
|
|
3673
3816
|
z.object({ id: entryId, kind: z.literal("vpn"), config: VpnConfigSchema }),
|
|
3817
|
+
// A geo exit (ExitConfigSchema). Same interface-name derivation as vpn, and deliberately NOT a vpn arm:
|
|
3818
|
+
// it routes nothing into the main table, so the full-tunnel warning the vpn kind carries stays true.
|
|
3819
|
+
z.object({ id: entryId, kind: z.literal("exit"), config: ExitConfigSchema }),
|
|
3674
3820
|
// The in-sandbox Docker Engine (baked into the base image, dormant by default). Its `--privileged` runtime
|
|
3675
3821
|
// directive is not in the config and never will be: dockerd does not work without it (see the handler's
|
|
3676
3822
|
// isPrivileged), so a switch there would offer a broken sandbox as a choice. What IS optional lives in
|
|
@@ -3687,6 +3833,10 @@ export const CapabilitySchema = z.discriminatedUnion("kind", [
|
|
|
3687
3833
|
// precedent, with the prefix because these two are the only capability kinds that mint providers and they
|
|
3688
3834
|
// want opposite ability records (an ACP agent owns its own loop; an endpoint runs the full Claude Code one).
|
|
3689
3835
|
z.object({ id: entryId, kind: z.literal("endpoint"), config: EndpointConfigSchema }),
|
|
3836
|
+
// A model the sandbox downloads and serves itself (LocalModelConfigSchema). Deliberately minting the SAME
|
|
3837
|
+
// `endpoint/<id>` provider ids as the endpoint kind: to every consumer it IS an endpoint, one the daemon
|
|
3838
|
+
// happens to operate, so a second provider namespace would be a second code path for the same turns.
|
|
3839
|
+
z.object({ id: entryId, kind: z.literal("localmodel"), config: LocalModelConfigSchema }),
|
|
3690
3840
|
// The sandbox's USDC wallet (WalletConfigSchema), one per sandbox; the key never enters the container.
|
|
3691
3841
|
z.object({ id: entryId, kind: z.literal("wallet"), config: WalletConfigSchema }),
|
|
3692
3842
|
]);
|
|
@@ -4142,6 +4292,138 @@ export const ForticlientConnectionSchema = z.object({
|
|
|
4142
4292
|
export type ForticlientConnection = z.infer<typeof ForticlientConnectionSchema>;
|
|
4143
4293
|
export const ForticlientImportSchema = z.object({ connections: z.array(ForticlientConnectionSchema) });
|
|
4144
4294
|
|
|
4295
|
+
// ---- exit: live state, the catalog, and the observation that makes a switch true ----
|
|
4296
|
+
// The manifest says which exits EXIST; this says which are up, where they come out, and what the world sees.
|
|
4297
|
+
// Read off the machine and off the wire, never remembered: an exit the agent stopped from a shell and one the
|
|
4298
|
+
// UI stopped read identically, and a daemon restart observes the truth rather than a stale guess.
|
|
4299
|
+
|
|
4300
|
+
export const ExitStateSchema = z.enum([
|
|
4301
|
+
// Carrying traffic: the proxy is listening and the last check came out where it was asked to.
|
|
4302
|
+
"up",
|
|
4303
|
+
// Coming up, or moving to another country. The proxy port may already be open and not yet where you want.
|
|
4304
|
+
"starting",
|
|
4305
|
+
// Configured and idle. The resting state, and the default one: exits are not held open for nothing.
|
|
4306
|
+
"down",
|
|
4307
|
+
// The client isn't installed yet (tor, openvpn): the capability's image fragment needs an owner rebuild.
|
|
4308
|
+
"unavailable",
|
|
4309
|
+
// The last start or switch failed; `detail` carries the reason.
|
|
4310
|
+
"failed",
|
|
4311
|
+
]);
|
|
4312
|
+
export type ExitState = z.infer<typeof ExitStateSchema>;
|
|
4313
|
+
|
|
4314
|
+
/* WHAT THE WORLD SEES, fetched THROUGH the exit's own proxy. This is the load-bearing type of the whole
|
|
4315
|
+
* feature: "switch to Germany" is worth nothing as a report that a tunnel came up, and worth everything as a
|
|
4316
|
+
* report that the egress address is now German. Every start, use and rotate ends by producing one of these,
|
|
4317
|
+
* and a switch that cannot produce one fails instead of quietly leaving traffic where it was. */
|
|
4318
|
+
export const ExitObservationSchema = z.object({
|
|
4319
|
+
ip: z.string(),
|
|
4320
|
+
// Absent when the lookup answered with an address but no country: a switch is judged on the country when
|
|
4321
|
+
// one is known, and on the address having CHANGED when it is not.
|
|
4322
|
+
country: z.string().optional(),
|
|
4323
|
+
countryName: z.string().optional(),
|
|
4324
|
+
});
|
|
4325
|
+
export type ExitObservation = z.infer<typeof ExitObservationSchema>;
|
|
4326
|
+
|
|
4327
|
+
/* One country an exit can come out of, as the picker and `exit countries` render it. `servers` and `share`
|
|
4328
|
+
* are what stop a country list being a lie: Tor lists 52 countries and a third of them are one underpowered
|
|
4329
|
+
* relay, so the ranking has to carry how much is actually there, not just that the flag exists. */
|
|
4330
|
+
export const ExitPointSchema = z.object({
|
|
4331
|
+
country: z.string(),
|
|
4332
|
+
countryName: z.string(),
|
|
4333
|
+
// How many relays/servers this provider has there right now.
|
|
4334
|
+
servers: z.number(),
|
|
4335
|
+
// This country's share of the provider's total exit capacity, 0..1. Used to sort and to grey out the
|
|
4336
|
+
// countries that technically exist and practically do not.
|
|
4337
|
+
share: z.number().optional(),
|
|
4338
|
+
});
|
|
4339
|
+
export type ExitPoint = z.infer<typeof ExitPointSchema>;
|
|
4340
|
+
export const ExitCountriesSchema = z.object({
|
|
4341
|
+
countries: z.array(ExitPointSchema),
|
|
4342
|
+
// Whether this list came off the provider live or out of the baked fallback (no network, or the provider
|
|
4343
|
+
// is down). The picker says so rather than presenting a stale list as current.
|
|
4344
|
+
live: z.boolean(),
|
|
4345
|
+
});
|
|
4346
|
+
|
|
4347
|
+
export const ExitLinkSchema = z.object({
|
|
4348
|
+
id: z.string(),
|
|
4349
|
+
provider: ExitProviderSchema,
|
|
4350
|
+
state: ExitStateSchema,
|
|
4351
|
+
// The SOCKS endpoint callers point at. Fixed per exit and stable across country switches, which is what
|
|
4352
|
+
// lets a long task change country halfway without reconfiguring anything downstream.
|
|
4353
|
+
proxy: z.string(),
|
|
4354
|
+
// The country ASKED for (manifest preference, or the last `use`). Absent = provider's choice.
|
|
4355
|
+
country: z.string().optional(),
|
|
4356
|
+
// The country actually OBSERVED at the last check, and the address behind it. These two disagreeing is
|
|
4357
|
+
// the single most useful fault signal this feature has, so they are separate fields, never merged.
|
|
4358
|
+
observedCountry: z.string().optional(),
|
|
4359
|
+
ip: z.string().optional(),
|
|
4360
|
+
// Epoch ms of the observation above, so a stale reading can be rendered as stale.
|
|
4361
|
+
checkedAt: z.number().optional(),
|
|
4362
|
+
// The tunnel interface, for the providers that have one (vpngate, wireguard). Tor has none by design.
|
|
4363
|
+
interface: z.string().optional(),
|
|
4364
|
+
since: z.number().optional(),
|
|
4365
|
+
autoStart: z.boolean(),
|
|
4366
|
+
detail: z.string().optional(),
|
|
4367
|
+
});
|
|
4368
|
+
export type ExitLink = z.infer<typeof ExitLinkSchema>;
|
|
4369
|
+
export const ExitListSchema = z.object({ links: z.array(ExitLinkSchema) });
|
|
4370
|
+
|
|
4371
|
+
/* WHERE EACH FREE PROVIDER CAN ACTUALLY COME OUT, as measured, and the reason it lives in the contract rather
|
|
4372
|
+
* than in the daemon: two consumers need the same answer and must not drift. The daemon uses it as the
|
|
4373
|
+
* FALLBACK catalog when a provider's own list cannot be fetched; the add form uses it to fill the country
|
|
4374
|
+
* picker, so a user chooses from a list instead of guessing a code and finding out later that nothing serves
|
|
4375
|
+
* it. A second copy of these numbers would let the picker offer a country the driver cannot dial.
|
|
4376
|
+
*
|
|
4377
|
+
* `share` is the country's slice of the provider's exit capacity, and it is the number that matters. A third
|
|
4378
|
+
* of Tor's fifty-two countries are one overloaded relay behind a flag; ranking by relay COUNT alone would put
|
|
4379
|
+
* the United States first on 1,171 slow relays when the Netherlands carries three times the traffic on half as
|
|
4380
|
+
* many. Both surfaces sort on this so the top of the list is the part that works.
|
|
4381
|
+
*
|
|
4382
|
+
* Measured 2026-08-21 from onionoo.torproject.org and vpngate.net's public CSV. Stale by construction, which
|
|
4383
|
+
* is exactly why the daemon prefers a live fetch and labels this one as not-live when it falls back to it. */
|
|
4384
|
+
export const TOR_EXIT_COUNTRIES: readonly ExitPoint[] = [
|
|
4385
|
+
{ country: "NL", countryName: "Netherlands", servers: 607, share: 0.304 },
|
|
4386
|
+
{ country: "DE", countryName: "Germany", servers: 415, share: 0.242 },
|
|
4387
|
+
{ country: "SE", countryName: "Sweden", servers: 344, share: 0.14 },
|
|
4388
|
+
{ country: "US", countryName: "United States", servers: 1171, share: 0.097 },
|
|
4389
|
+
{ country: "AT", countryName: "Austria", servers: 123, share: 0.054 },
|
|
4390
|
+
{ country: "LU", countryName: "Luxembourg", servers: 92, share: 0.033 },
|
|
4391
|
+
{ country: "FR", countryName: "France", servers: 63, share: 0.032 },
|
|
4392
|
+
{ country: "NO", countryName: "Norway", servers: 54, share: 0.026 },
|
|
4393
|
+
{ country: "RO", countryName: "Romania", servers: 71, share: 0.011 },
|
|
4394
|
+
{ country: "DK", countryName: "Denmark", servers: 15, share: 0.007 },
|
|
4395
|
+
{ country: "HU", countryName: "Hungary", servers: 20, share: 0.006 },
|
|
4396
|
+
{ country: "IT", countryName: "Italy", servers: 15, share: 0.006 },
|
|
4397
|
+
{ country: "UA", countryName: "Ukraine", servers: 23, share: 0.006 },
|
|
4398
|
+
{ country: "CH", countryName: "Switzerland", servers: 23, share: 0.006 },
|
|
4399
|
+
{ country: "IS", countryName: "Iceland", servers: 23, share: 0.003 },
|
|
4400
|
+
{ country: "PL", countryName: "Poland", servers: 8, share: 0.002 },
|
|
4401
|
+
{ country: "BG", countryName: "Bulgaria", servers: 17, share: 0.002 },
|
|
4402
|
+
{ country: "GB", countryName: "United Kingdom", servers: 8, share: 0.002 },
|
|
4403
|
+
{ country: "FI", countryName: "Finland", servers: 13, share: 0.002 },
|
|
4404
|
+
];
|
|
4405
|
+
|
|
4406
|
+
// VPN Gate, and its shape is the honest headline: 87% of its pool is Japan and Korea. That is not a defect to
|
|
4407
|
+
// hide behind a long country list, it is the reason to have it, Tor's Asian exit capacity is close to nothing,
|
|
4408
|
+
// so the two providers cover each other rather than overlapping.
|
|
4409
|
+
export const VPNGATE_EXIT_COUNTRIES: readonly ExitPoint[] = [
|
|
4410
|
+
{ country: "JP", countryName: "Japan", servers: 46, share: 0.48 },
|
|
4411
|
+
{ country: "KR", countryName: "Korea, South", servers: 37, share: 0.39 },
|
|
4412
|
+
{ country: "VN", countryName: "Vietnam", servers: 3, share: 0.03 },
|
|
4413
|
+
{ country: "TH", countryName: "Thailand", servers: 2, share: 0.02 },
|
|
4414
|
+
{ country: "RU", countryName: "Russia", servers: 2, share: 0.02 },
|
|
4415
|
+
{ country: "RO", countryName: "Romania", servers: 1, share: 0.01 },
|
|
4416
|
+
{ country: "MX", countryName: "Mexico", servers: 1, share: 0.01 },
|
|
4417
|
+
{ country: "IN", countryName: "India", servers: 1, share: 0.01 },
|
|
4418
|
+
{ country: "CN", countryName: "China", servers: 1, share: 0.01 },
|
|
4419
|
+
{ country: "BY", countryName: "Belarus", servers: 1, share: 0.01 },
|
|
4420
|
+
];
|
|
4421
|
+
|
|
4422
|
+
export const ExitIdParamSchema = z.object({ id: z.string() });
|
|
4423
|
+
// POST /exit/{id}/use body. An absent country means "let the provider choose", the same thing an absent
|
|
4424
|
+
// `country` in the manifest means, so clearing a country is expressible rather than only setting one.
|
|
4425
|
+
export const ExitUseInputSchema = z.object({ id: z.string(), country: CountryCodeSchema.optional() });
|
|
4426
|
+
|
|
4145
4427
|
// Browse an extension/plugin registry (a git repo with .claude-plugin/marketplace.json, see
|
|
4146
4428
|
// @intentic/registry for the format). POST so the optional token for a private registry never rides a URL or
|
|
4147
4429
|
// an access log.
|
|
@@ -5431,14 +5713,14 @@ export const PortSummarySchema = z.object({
|
|
|
5431
5713
|
// processes, published container ports), the previewable set; `system` = the sandbox's own machinery
|
|
5432
5714
|
// (agent runtimes, translator, dockerd, sshd), listed for transparency but nobody previews it.
|
|
5433
5715
|
kind: z.enum(["workspace", "system"]),
|
|
5434
|
-
/* WHAT IS ON THIS PORT, IN WORDS
|
|
5716
|
+
/* WHAT IS ON THIS PORT, IN WORDS: resolved by the daemon (ports/port-identity.ts), because the two facts
|
|
5435
5717
|
* that attribute a listener (the panel key → extension index, the workspace root) exist there and nowhere
|
|
5436
5718
|
* else. `title` is what a person would call it ("Vite dev server", "Sandbox service", "Container port"),
|
|
5437
|
-
* `purpose` is the one sentence a row shows under it, and `origin` says who put it there
|
|
5719
|
+
* `purpose` is the one sentence a row shows under it, and `origin` says who put it there, which is what
|
|
5438
5720
|
* the reader is really asking when they ask what a port is: mine, my agent's, or the box's own.
|
|
5439
5721
|
*
|
|
5440
5722
|
* All three are required. A listener nothing can explain still gets a name ("Unclaimed port") and a
|
|
5441
|
-
* sentence that says so out loud, because the alternative
|
|
5723
|
+
* sentence that says so out loud, because the alternative (a raw argv, or nothing) is what made this
|
|
5442
5724
|
* view unreadable, and the button beside the row publishes the port to the internet. */
|
|
5443
5725
|
title: z.string(),
|
|
5444
5726
|
purpose: z.string(),
|
|
@@ -6596,6 +6878,29 @@ export const UsageTurnSchema = z.object({
|
|
|
6596
6878
|
*
|
|
6597
6879
|
* Absent ⇒ as for `searchCalls`. */
|
|
6598
6880
|
openingSearches: z.number().optional(),
|
|
6881
|
+
/* WHAT THE COMPLEXITY JUDGE SAID ABOUT THIS TURN, and whether anything was done about it. The three fields
|
|
6882
|
+
* automatic tier selection is calibrated from, and the reason it can ship in shadow at all.
|
|
6883
|
+
*
|
|
6884
|
+
* They live on the SPEND ledger rather than in a log of their own because the question they exist to answer
|
|
6885
|
+
* is a question about money: what did the turns we would have downgraded actually cost, and what did the
|
|
6886
|
+
* ones we did downgrade cost instead. A separate log would have to be joined back to this one on every
|
|
6887
|
+
* read, and the join key (a turn) is already the row.
|
|
6888
|
+
*
|
|
6889
|
+
* `tierScore` is 0..1 from judgeComplexity, comparable against FAST_CEILING, which is the cutoff it was
|
|
6890
|
+
* judged against at the time. Absent ⇒ the judge did not run (settings.autoTier "off", or a row written
|
|
6891
|
+
* before this existed), which is NOT the same as a turn that scored zero.
|
|
6892
|
+
*
|
|
6893
|
+
* `tierRules` is which named features fired, and it is the half that makes the ledger analysable rather
|
|
6894
|
+
* than merely tallyable: a score says a threshold was crossed, the rules say which feature is doing the
|
|
6895
|
+
* work, and re-fitting the weights needs the second. Bounded by construction, there are ~19 of them.
|
|
6896
|
+
*
|
|
6897
|
+
* `tierRouted` is whether the turn ACTUALLY ran on the cheap rung. It is not implied by the score: a turn
|
|
6898
|
+
* judged fast still runs standard in shadow mode, and still runs standard in `on` mode when the provider
|
|
6899
|
+
* publishes nothing cheaper than the user's pick. Reading the score as the decision would report savings
|
|
6900
|
+
* that were never made. */
|
|
6901
|
+
tierScore: z.number().optional(),
|
|
6902
|
+
tierRules: z.array(z.string()).optional(),
|
|
6903
|
+
tierRouted: z.boolean().optional(),
|
|
6599
6904
|
});
|
|
6600
6905
|
export type UsageTurn = z.infer<typeof UsageTurnSchema>;
|
|
6601
6906
|
|
package/src/search-globs.test.ts
CHANGED
|
@@ -11,7 +11,7 @@ test("an empty field scopes nothing", () => {
|
|
|
11
11
|
test("a bare name is a file as well as a folder, at any depth", () => {
|
|
12
12
|
expect(includeGlobs(`package.json`).globs).toEqual([`**/package.json`, `**/package.json/**`]);
|
|
13
13
|
expect(includeGlobs(`docs`).globs).toEqual([`**/docs`, `**/docs/**`]);
|
|
14
|
-
// A trailing slash is noise
|
|
14
|
+
// A trailing slash is noise: the folder form is generated either way.
|
|
15
15
|
expect(includeGlobs(`docs/`).globs).toEqual([`**/docs`, `**/docs/**`]);
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -41,6 +41,6 @@ test("a leading ! excludes instead", () => {
|
|
|
41
41
|
globs: [`**/src`, `**/src/**`],
|
|
42
42
|
notGlobs: [`**/*.test.ts`, `**/*.test.ts/**`],
|
|
43
43
|
});
|
|
44
|
-
// A lone "!" excludes nothing
|
|
44
|
+
// A lone "!" excludes nothing: expanded, it would have matched everything.
|
|
45
45
|
expect(includeGlobs(`!`)).toEqual({ globs: [], notGlobs: [] });
|
|
46
46
|
});
|
package/src/share-paths.test.ts
CHANGED
|
@@ -17,7 +17,7 @@ describe("the readable half", () => {
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
// A title in a writing system this alphabet cannot carry leaves the random half standing alone, which is
|
|
20
|
-
// still a perfectly good id
|
|
20
|
+
// still a perfectly good id, and a better outcome than an address full of percent-encoding.
|
|
21
21
|
it("falls back to the random half when a title has no letters to give", () => {
|
|
22
22
|
expect(shareStem("日本語のタイトル")).toBe("");
|
|
23
23
|
expect(shareId("日本語のタイトル", "3f9c")).toBe("3f9c");
|
package/src/title.test.ts
CHANGED
|
@@ -7,13 +7,13 @@ import { deriveTitle } from "./title.js";
|
|
|
7
7
|
* politeness, or stopped mid-syllable. */
|
|
8
8
|
|
|
9
9
|
test("spends the budget on the ask rather than on the politeness in front of it", () => {
|
|
10
|
-
// `Can you please fix the auth…` is what the naive cut produced
|
|
10
|
+
// `Can you please fix the auth…` is what the naive cut produced: three of its five words are ceremony.
|
|
11
11
|
expect(deriveTitle("Can you please fix the auth tests?")).toBe("Fix the auth tests?");
|
|
12
12
|
expect(deriveTitle("Hey, can you please look at the flaky test")).toBe("Look at the flaky test");
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
test("keeps a greeting whole when peeling it off would leave a fragment", () => {
|
|
16
|
-
// `Hi there` is not a conversation about `there`, and `So what?` is not one about `what
|
|
16
|
+
// `Hi there` is not a conversation about `there`, and `So what?` is not one about `what?`: a one-word
|
|
17
17
|
// remnant means the opener was carrying the sentence, so the line stands as the user wrote it.
|
|
18
18
|
expect(deriveTitle("Hi there")).toBe("Hi there");
|
|
19
19
|
expect(deriveTitle("So what?")).toBe("So what?");
|
|
@@ -35,7 +35,7 @@ test("titles a pasted stack trace after the sentence the user wrote around it",
|
|
|
35
35
|
|
|
36
36
|
test("skips a greeting line and takes the ask from the line below it", () => {
|
|
37
37
|
// A line that unwinds to nothing was throat-clearing; the ask is further down. The naive rule stopped at
|
|
38
|
-
// the greeting and named every such conversation `Hey, quick one
|
|
38
|
+
// the greeting and named every such conversation `Hey, quick one -`.
|
|
39
39
|
expect(deriveTitle("Hey, quick one —\n\nWhy does the tab title truncate mid-word?")).toBe("Why does the tab title truncate mid-word?");
|
|
40
40
|
});
|
|
41
41
|
|
|
@@ -61,7 +61,7 @@ test("cuts on a word boundary instead of mid-syllable", () => {
|
|
|
61
61
|
|
|
62
62
|
test("keeps a cut it cannot put on a word boundary inside the budget anyway", () => {
|
|
63
63
|
/* The sibling of the case above: a long unbroken token straddling the cut leaves no space late enough to
|
|
64
|
-
* back off to, so the length clamp alone ends the title
|
|
64
|
+
* back off to, so the length clamp alone ends the title, and it used to end it one character OVER. That
|
|
65
65
|
* character is not a cosmetic overflow, it is a 400 on the turn carrying the name, and it wedged the
|
|
66
66
|
* conversation for good: the browser stores the derived title before it sends, so every retry re-sent the
|
|
67
67
|
* same rejected one. */
|
|
@@ -72,7 +72,7 @@ test("keeps a cut it cannot put on a word boundary inside the budget anyway", ()
|
|
|
72
72
|
// Asserted THROUGH the contract rather than against a repeated literal: the clamp and the cap drifting
|
|
73
73
|
// apart is the entire bug, and a second copy of the number is how they drift.
|
|
74
74
|
expect(AgentTurnSchema.safeParse({ prompt, title }).success).toBe(true);
|
|
75
|
-
// Not a property of that one sentence
|
|
75
|
+
// Not a property of that one sentence: any token wide enough to swallow the window does it.
|
|
76
76
|
const wide = `Investigate ${"X".repeat(120)} please`;
|
|
77
77
|
expect(AgentTurnSchema.safeParse({ prompt: wide, title: deriveTitle(wide) }).success).toBe(true);
|
|
78
78
|
});
|
|
@@ -111,7 +111,7 @@ test("names a prompt that is nothing but a paste after what was pasted", () => {
|
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
test("never returns empty for a prompt that has any content at all", () => {
|
|
114
|
-
// A greeting with no ask behind it, and a prompt with no letters in it
|
|
114
|
+
// A greeting with no ask behind it, and a prompt with no letters in it: both still have to name a tab.
|
|
115
115
|
expect(deriveTitle("Hey!")).toBe("Hey!");
|
|
116
116
|
expect(deriveTitle("!!!")).toBe("!!!");
|
|
117
117
|
});
|
|
@@ -121,7 +121,7 @@ test("reads past quoted material to the user's own words", () => {
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
test("skips past-work narration to the instruction behind it", () => {
|
|
124
|
-
// `We have recently added…` is the scene, not the ask
|
|
124
|
+
// `We have recently added…` is the scene, not the ask: titling from it names every such conversation
|
|
125
125
|
// after last week's work. The instruction further in is what the conversation is about.
|
|
126
126
|
expect(deriveTitle("We have recently added iq map and iq deps commands. Now let's also add a health contract for the daemon.")).toBe(
|
|
127
127
|
"Add a health contract for the daemon",
|
|
@@ -134,13 +134,13 @@ test("skips narration to an outright question", () => {
|
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
test("keeps narration when nothing behind it is unmistakably the ask", () => {
|
|
137
|
-
// A hazy follow-up is not worth skipping for
|
|
137
|
+
// A hazy follow-up is not worth skipping for: better a title about last week's work than one about `it`.
|
|
138
138
|
expect(deriveTitle("We migrated the board to SSE last week. It feels slower since.")).toBe("We migrated the board to SSE last week");
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
test("keeps a declarative problem report even when advice follows it", () => {
|
|
142
142
|
// Only NARRATION is skippable. A problem statement is the ask, and the imperative behind it is merely a
|
|
143
|
-
// pointer
|
|
143
|
+
// pointer: `Check the broadcast path` names a step, not the conversation.
|
|
144
144
|
expect(deriveTitle("The fleet board flickers when agents land. Check the broadcast path.")).toBe("The fleet board flickers when agents land");
|
|
145
145
|
});
|
|
146
146
|
|
package/src/title.ts
CHANGED
|
@@ -104,7 +104,7 @@ const proseLines = (prompt: string): string[] => {
|
|
|
104
104
|
*
|
|
105
105
|
* Three outcomes, and the difference between them is the whole point. A remnant of SEVERAL words is the ask,
|
|
106
106
|
* `Can you please fix the auth tests?` was carrying `fix the auth tests?`. A remnant of NOTHING was pure
|
|
107
|
-
* greeting (`Hey, quick one
|
|
107
|
+
* greeting (`Hey, quick one -`), and returning empty is how the caller learns to read the next line instead.
|
|
108
108
|
* A remnant of ONE word means the opener was meaningful. `Hi there` is not a conversation about `there`,
|
|
109
109
|
* and `So what?` is not one about `what?`, so the line stands as written. */
|
|
110
110
|
const withoutOpener = (line: string): string => {
|
package/src/tunnel-ids.test.ts
CHANGED
|
@@ -34,11 +34,11 @@ test("port slots are a fixed-size pool of DNS-safe labels, stable per token", ()
|
|
|
34
34
|
expect(portSlotsFromToken(TOKEN)).toEqual(slots);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
/* The whole point of the salt. The sandbox id is public
|
|
38
|
-
* of every preview link its owner has shared
|
|
37
|
+
/* The whole point of the salt. The sandbox id is public: it is the leading label of the sandbox's own URL and
|
|
38
|
+
* of every preview link its owner has shared, so anything derived from the id ALONE is derivable by whoever
|
|
39
39
|
* holds one of those links. Slots must not be: knowing a sandbox's id must not tell you where its forwarded
|
|
40
40
|
* ports live. */
|
|
41
|
-
test("slots are not derivable from the sandbox id
|
|
41
|
+
test("slots are not derivable from the sandbox id: only from the token behind it", () => {
|
|
42
42
|
expect(portSlotsFromToken(OTHER)).not.toEqual(portSlotsFromToken(TOKEN));
|
|
43
43
|
// No slot leaks the id it will be paired with in `port-<slot>-<sandboxId>`.
|
|
44
44
|
const id = sandboxIdFromToken(TOKEN);
|