@intentic/sandbox-contract 1.224.0 → 1.226.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.
Files changed (109) hide show
  1. package/README.md +14 -14
  2. package/dist/agent-catalog.d.ts +2 -0
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +19 -3
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/chores/chores.js +42 -42
  7. package/dist/chores/chores.js.map +1 -1
  8. package/dist/chores/extension-update.js +2 -2
  9. package/dist/chores/extension-update.js.map +1 -1
  10. package/dist/chores/fix-deps.js +1 -1
  11. package/dist/chores/fix-deps.js.map +1 -1
  12. package/dist/chores/probes.js +1 -1
  13. package/dist/chores/probes.js.map +1 -1
  14. package/dist/chores/verdict.js +2 -2
  15. package/dist/chores/verdict.js.map +1 -1
  16. package/dist/contracts/capabilities.contract.d.ts +41 -0
  17. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  18. package/dist/contracts/exit.contract.d.ts +80 -0
  19. package/dist/contracts/exit.contract.d.ts.map +1 -0
  20. package/dist/contracts/exit.contract.js +13 -0
  21. package/dist/contracts/exit.contract.js.map +1 -0
  22. package/dist/contracts/settings.contract.d.ts +12 -0
  23. package/dist/contracts/settings.contract.d.ts.map +1 -1
  24. package/dist/events.d.ts +4 -4
  25. package/dist/events.d.ts.map +1 -1
  26. package/dist/events.js +5 -5
  27. package/dist/events.js.map +1 -1
  28. package/dist/fast-tier.d.ts +9 -0
  29. package/dist/fast-tier.d.ts.map +1 -0
  30. package/dist/fast-tier.js +19 -0
  31. package/dist/fast-tier.js.map +1 -0
  32. package/dist/history-state.js +3 -3
  33. package/dist/history-state.js.map +1 -1
  34. package/dist/index.d.ts +203 -68
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +5 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/model-order.d.ts +1 -0
  39. package/dist/model-order.d.ts.map +1 -1
  40. package/dist/model-order.js +5 -0
  41. package/dist/model-order.js.map +1 -1
  42. package/dist/output-fields.d.ts.map +1 -1
  43. package/dist/output-fields.js +2 -2
  44. package/dist/output-fields.js.map +1 -1
  45. package/dist/prompt-complexity.d.ts +19 -0
  46. package/dist/prompt-complexity.d.ts.map +1 -0
  47. package/dist/prompt-complexity.js +83 -0
  48. package/dist/prompt-complexity.js.map +1 -0
  49. package/dist/publish-drafts.js +2 -2
  50. package/dist/publish-drafts.js.map +1 -1
  51. package/dist/schemas.d.ts +214 -0
  52. package/dist/schemas.d.ts.map +1 -1
  53. package/dist/schemas.js +113 -2
  54. package/dist/schemas.js.map +1 -1
  55. package/dist/workflow-faults.js +3 -3
  56. package/dist/workflow-faults.js.map +1 -1
  57. package/dist/workspace-state.d.ts +20 -20
  58. package/dist/workspace-state.d.ts.map +1 -1
  59. package/dist/workspace-state.js +20 -20
  60. package/dist/workspace-state.js.map +1 -1
  61. package/package.json +5 -5
  62. package/src/agent-catalog.test.ts +72 -27
  63. package/src/agent-catalog.ts +91 -3
  64. package/src/agent-run-model.test.ts +3 -3
  65. package/src/capability-ledger.test.ts +34 -15
  66. package/src/chores/chores.test.ts +1 -1
  67. package/src/chores/chores.ts +42 -42
  68. package/src/chores/digest.test.ts +1 -1
  69. package/src/chores/extension-update.ts +2 -2
  70. package/src/chores/fix-deps.ts +1 -1
  71. package/src/chores/probes.test.ts +6 -6
  72. package/src/chores/probes.ts +1 -1
  73. package/src/chores/stack.test.ts +3 -3
  74. package/src/chores/verdict.test.ts +20 -20
  75. package/src/chores/verdict.ts +2 -2
  76. package/src/contract-lock.test.ts +1 -1
  77. package/src/contracts/exit.contract.ts +42 -0
  78. package/src/contracts/providers.contract.ts +1 -1
  79. package/src/conversation-ids.ts +1 -1
  80. package/src/events.test.ts +3 -3
  81. package/src/events.ts +6 -6
  82. package/src/fast-tier.test.ts +88 -0
  83. package/src/fast-tier.ts +72 -0
  84. package/src/history-state.ts +3 -3
  85. package/src/hostnames.test.ts +1 -1
  86. package/src/index.ts +5 -0
  87. package/src/model-order.test.ts +11 -11
  88. package/src/model-order.ts +22 -0
  89. package/src/output-fields.ts +2 -2
  90. package/src/path-refs.test.ts +4 -4
  91. package/src/prompt-complexity.test.ts +160 -0
  92. package/src/prompt-complexity.ts +271 -0
  93. package/src/publish-drafts.ts +2 -2
  94. package/src/quick-model.test.ts +11 -11
  95. package/src/routes.test.ts +11 -5
  96. package/src/runtime-state.test.ts +1 -1
  97. package/src/schemas.test.ts +8 -8
  98. package/src/schemas.ts +311 -6
  99. package/src/search-globs.test.ts +2 -2
  100. package/src/share-paths.test.ts +1 -1
  101. package/src/title.test.ts +9 -9
  102. package/src/title.ts +1 -1
  103. package/src/tunnel-ids.test.ts +3 -3
  104. package/src/versions.test.ts +3 -3
  105. package/src/versions.ts +1 -1
  106. package/src/workflow-faults.test.ts +6 -6
  107. package/src/workflow-faults.ts +3 -3
  108. package/src/workspace-state.test.ts +32 -32
  109. package/src/workspace-state.ts +20 -20
@@ -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 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. */
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 a diff that only ever said "the list moved",
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 `{}` one field without a
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 a workspace that has never written settings parses", () => {
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 tolerance is for absence, not for garbage", () => {
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 to hide an advisory badge. */
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 strongSwan wants the prefix, and the message says so.
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 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).`,
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) /etc/docker/daemon.json, which dockerd reads at start. Changing one
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 USDC's own precision)");
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 resolved by the daemon (ports/port-identity.ts), because the two facts
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 which is what
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 a raw argv, or nothing is what made this
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
 
@@ -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 the folder form is generated either way.
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 expanded, it would have matched everything.
44
+ // A lone "!" excludes nothing: expanded, it would have matched everything.
45
45
  expect(includeGlobs(`!`)).toEqual({ globs: [], notGlobs: [] });
46
46
  });
@@ -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 and a better outcome than an address full of percent-encoding.
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 three of its five words are ceremony.
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?` a one-word
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 and it used to end it one character OVER. That
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 any token wide enough to swallow the window does it.
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 both still have to name a tab.
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 titling from it names every such conversation
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 better a title about last week's work than one about `it`.
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 `Check the broadcast path` names a step, not the conversation.
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 —`), and returning empty is how the caller learns to read the next line instead.
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 => {
@@ -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 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
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 only from the token behind it", () => {
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);