@intentic/sandbox-contract 1.235.0 → 1.237.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 (68) hide show
  1. package/dist/chores/chores.d.ts.map +1 -1
  2. package/dist/chores/chores.js +2 -1
  3. package/dist/chores/chores.js.map +1 -1
  4. package/dist/chores/index.d.ts +1 -0
  5. package/dist/chores/index.d.ts.map +1 -1
  6. package/dist/chores/index.js +1 -0
  7. package/dist/chores/index.js.map +1 -1
  8. package/dist/chores/probes.d.ts.map +1 -1
  9. package/dist/chores/probes.js +4 -3
  10. package/dist/chores/probes.js.map +1 -1
  11. package/dist/chores/workspace-scope.d.ts +4 -0
  12. package/dist/chores/workspace-scope.d.ts.map +1 -0
  13. package/dist/chores/workspace-scope.js +4 -0
  14. package/dist/chores/workspace-scope.js.map +1 -0
  15. package/dist/contracts/agent.contract.d.ts +2 -0
  16. package/dist/contracts/agent.contract.d.ts.map +1 -1
  17. package/dist/contracts/agents.contract.d.ts +6 -0
  18. package/dist/contracts/agents.contract.d.ts.map +1 -1
  19. package/dist/contracts/extensions.contract.d.ts +1 -2
  20. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  21. package/dist/contracts/extensions.contract.js +5 -5
  22. package/dist/contracts/extensions.contract.js.map +1 -1
  23. package/dist/contracts/runner.contract.d.ts +2 -0
  24. package/dist/contracts/runner.contract.d.ts.map +1 -1
  25. package/dist/contracts/workspace.contract.d.ts +1 -0
  26. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  27. package/dist/contracts/workspace.contract.js +2 -2
  28. package/dist/contracts/workspace.contract.js.map +1 -1
  29. package/dist/events.d.ts +8 -0
  30. package/dist/events.d.ts.map +1 -1
  31. package/dist/events.js +12 -2
  32. package/dist/events.js.map +1 -1
  33. package/dist/index.d.ts +10 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/ingress-contract.d.ts.map +1 -1
  36. package/dist/ingress-contract.js.map +1 -1
  37. package/dist/ingress-protocol.d.ts +16 -0
  38. package/dist/ingress-protocol.d.ts.map +1 -0
  39. package/dist/ingress-protocol.js +263 -0
  40. package/dist/ingress-protocol.js.map +1 -0
  41. package/dist/schemas/agents.d.ts.map +1 -1
  42. package/dist/schemas/agents.js +5 -5
  43. package/dist/schemas/agents.js.map +1 -1
  44. package/dist/schemas/extension-updates.d.ts +2 -3
  45. package/dist/schemas/extension-updates.d.ts.map +1 -1
  46. package/dist/schemas/extension-updates.js +4 -3
  47. package/dist/schemas/extension-updates.js.map +1 -1
  48. package/dist/schemas/workspace-tree.d.ts +1 -0
  49. package/dist/schemas/workspace-tree.d.ts.map +1 -1
  50. package/dist/schemas/workspace-tree.js +8 -1
  51. package/dist/schemas/workspace-tree.js.map +1 -1
  52. package/package.json +4 -4
  53. package/src/chores/chores.ts +2 -1
  54. package/src/chores/index.ts +1 -0
  55. package/src/chores/probes.test.ts +12 -0
  56. package/src/chores/probes.ts +5 -3
  57. package/src/chores/workspace-scope.ts +12 -0
  58. package/src/contracts/extensions.contract.ts +7 -6
  59. package/src/contracts/workspace.contract.ts +3 -2
  60. package/src/events.ts +35 -4
  61. package/src/hostnames.ts +2 -2
  62. package/src/ingress-contract.ts +4 -2
  63. package/src/ingress-protocol.test.ts +509 -0
  64. package/src/ingress-protocol.ts +574 -0
  65. package/src/schemas/agents.ts +20 -21
  66. package/src/schemas/extension-updates.ts +9 -7
  67. package/src/schemas/workspace-tree.ts +13 -4
  68. package/src/tunnel-ids.ts +4 -4
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, test } from "vitest";
2
+ import { choreById } from "./chores.js";
2
3
  import { probeSpec } from "./probes.js";
3
4
  import { IDIOM_RULES } from "./stack.js";
5
+ import { WORKSPACE_ROOT_JSCPD_EXCLUDE_ARG, WORKSPACE_ROOT_RG_EXCLUDE_ARG } from "./workspace-scope.js";
4
6
 
5
7
  /* The parsers are the part of this library that faces someone else's output, so they are tested the way that
6
8
  * output actually arrives: real shapes, then the shapes that have historically broken things, a tool that
@@ -143,6 +145,11 @@ describe(`knip`, () => {
143
145
  });
144
146
 
145
147
  describe(`jscpd`, () => {
148
+ test(`the root-scoped command carries the reference-shelf prune argument`, () => {
149
+ expect(probeSpec(`jscpd`).command).toContain(WORKSPACE_ROOT_JSCPD_EXCLUDE_ARG);
150
+ expect(choreById(`duplication`)?.automation?.guard).toContain(WORKSPACE_ROOT_JSCPD_EXCLUDE_ARG);
151
+ });
152
+
146
153
  test(`takes the percentage of scanned lines and the biggest clones, longest first`, () => {
147
154
  const facts = parse(
148
155
  `jscpd`,
@@ -322,6 +329,11 @@ describe(`ui`, () => {
322
329
  describe(`the sweep's composed command`, () => {
323
330
  const stages = (): string[] => probeSpec(`ui`).command.split(`; `);
324
331
 
332
+ test(`both ripgrep entry points carry the root-scoped prune argument`, () => {
333
+ expect(probeSpec(`ui`).available).toContain(WORKSPACE_ROOT_RG_EXCLUDE_ARG);
334
+ expect(probeSpec(`ui`).command.split(WORKSPACE_ROOT_RG_EXCLUDE_ARG)).toHaveLength(3 + IDIOM_RULES.length);
335
+ });
336
+
325
337
  /* Given no path, ripgrep searches STDIN whenever stdin is not a TTY, which is exactly how a probe is spawned.
326
338
  * The sweep exited 0, printed its marker and matched nothing, in every repository, forever, which the marker
327
339
  * line cannot catch because the sweep really did run. It reproduces from a child process and never from an
@@ -10,6 +10,7 @@ import {
10
10
  UI_FRAMEWORKS,
11
11
  TAILWIND_PACKAGES,
12
12
  } from "./stack.js";
13
+ import { WORKSPACE_ROOT_JSCPD_EXCLUDE_ARG, WORKSPACE_ROOT_RG_EXCLUDE_ARG } from "./workspace-scope.js";
13
14
 
14
15
  /* THE PROBES, the measurements that cost a subprocess, declared once so the daemon that runs them and the panel
15
16
  * that explains them cannot disagree about what "outdated" meant.
@@ -348,7 +349,8 @@ const RULE_FILE_LIMIT = 500;
348
349
  * path reaches the chores no matter which tool produced it. */
349
350
  const SCAN_ROOT = `.`;
350
351
 
351
- const globArgs = (globs: readonly string[]): string => [...globs, ...SCAN_IGNORES].map((glob) => `-g '${glob}'`).join(` `);
352
+ const globArgs = (globs: readonly string[]): string =>
353
+ [...[...globs, ...SCAN_IGNORES].map((glob) => `-g '${glob}'`), WORKSPACE_ROOT_RG_EXCLUDE_ARG].join(` `);
352
354
 
353
355
  // `path:count` from `rg --count-matches`, normalised. Split at the LAST colon: a path may contain one, a count is
354
356
  // always the digits at the end.
@@ -526,7 +528,7 @@ export const PROBES: readonly ProbeSpec[] = [
526
528
  // `--threshold 100` so jscpd never fails the command on its own opinion of what is too much duplication,
527
529
  // that judgement is the chore's, made from the percentage, not the tool's exit code.
528
530
  command:
529
- `pnpm dlx jscpd --reporters json --output ${JSCPD_DIR} --min-lines 12 --threshold 100 . >/dev/null 2>&1; ` +
531
+ `pnpm dlx jscpd ${WORKSPACE_ROOT_JSCPD_EXCLUDE_ARG} --reporters json --output ${JSCPD_DIR} --min-lines 12 --threshold 100 . >/dev/null 2>&1; ` +
530
532
  `cat ${JSCPD_DIR}/jscpd-report.json 2>/dev/null`,
531
533
  parse: parseJscpd,
532
534
  },
@@ -576,7 +578,7 @@ export const PROBES: readonly ProbeSpec[] = [
576
578
  // Any manifest in the repo declaring a UI framework or Tailwind, not just the root's, a monorepo keeps
577
579
  // React in the app package and the root manifest is a handful of build tools.
578
580
  available:
579
- `rg -l --no-messages -g '**/package.json' -g '!**/node_modules/**' ` +
581
+ `rg -l --no-messages -g '**/package.json' -g '!**/node_modules/**' ${WORKSPACE_ROOT_RG_EXCLUDE_ARG} ` +
580
582
  `-e '[\\x22](${[...UI_FRAMEWORKS.flatMap((framework) => framework.packages), ...TAILWIND_PACKAGES].join(`|`)})[\\x22]\\s*:' . >/dev/null`,
581
583
  unavailable: `no package here declares a UI framework or Tailwind`,
582
584
  command: scanCommand(),
@@ -0,0 +1,12 @@
1
+ /* Maintenance probes normally run inside one discovered repository, where a `refs/` directory is ordinary
2
+ * project content. They also run once against the workspace root, where the same first segment is the reserved
3
+ * reference shelf and can hold hundreds of thousands of files. The daemon sets this variable only for that
4
+ * root scope; shell commands opt into the matching prune argument without baking the shelf's name into the
5
+ * browser-safe contract package. */
6
+ export const WORKSPACE_ROOT_EXCLUDE_ENV = `INTENTIC_WORKSPACE_ROOT_EXCLUDE`;
7
+
8
+ // Unquoted parameter expansion is intentional: when the variable is absent it contributes zero arguments;
9
+ // when present, each expression expands to one whitespace-free CLI argument. The daemon supplies the value
10
+ // from @intentic/workspace-ignore, never from user input.
11
+ export const WORKSPACE_ROOT_RG_EXCLUDE_ARG = `\${${WORKSPACE_ROOT_EXCLUDE_ENV}:+--glob=!/\${${WORKSPACE_ROOT_EXCLUDE_ENV}}/**}`;
12
+ export const WORKSPACE_ROOT_JSCPD_EXCLUDE_ARG = `\${${WORKSPACE_ROOT_EXCLUDE_ENV}:+--ignore=\${${WORKSPACE_ROOT_EXCLUDE_ENV}}/**}`;
@@ -12,7 +12,7 @@ import {
12
12
  ExtensionUpdatePolicyInputSchema,
13
13
  ExtensionUpdatePreviewSchema,
14
14
  ExtensionUpdatesCheckedSchema,
15
- ExtensionUsageInputSchema,
15
+ ExtensionUsageBatchSchema,
16
16
  WorkspaceExtensionCreatedSchema,
17
17
  WorkspaceExtensionCreateSchema,
18
18
  } from "../schemas/extension-updates.js";
@@ -80,16 +80,17 @@ export const extensionsContract = {
80
80
  .output(OkSchema),
81
81
  // The host reporting which declared routes it just let through. Written by the browser because that is where
82
82
  // the permission gate runs (apiImpl.ts), the daemon sees an extension's traffic as ordinary authenticated
83
- // requests and cannot tell which extension, or which declared entry, any of it belongs to.
83
+ // requests and cannot tell which extension, or which declared entry, any of it belongs to. Every extension's
84
+ // accumulated counts ride together so the bookkeeping is one request rather than a burst of them.
84
85
  recordUsage: oc
85
86
  .route({
86
87
  method: "POST",
87
- path: "/extensions/{id}/usage",
88
- summary: "Record what an extension just used",
88
+ path: "/extensions/usage",
89
+ summary: "Record what extensions just used",
89
90
  description:
90
- "Written by the app rather than measured by the daemon, because the permission gate runs in the browser: from the sandbox's side an extension's traffic is indistinguishable from anyone else's. This is how the record of which powers an extension actually exercises gets kept.",
91
+ "One batch written by the app rather than measured by the daemon, because the permission gate runs in the browser: from the sandbox's side extension traffic is indistinguishable from anyone else's. This is how the record of which powers each extension actually exercises gets kept without one reporting request per extension.",
91
92
  })
92
- .input(ExtensionUsageInputSchema)
93
+ .input(ExtensionUsageBatchSchema)
93
94
  .output(OkSchema),
94
95
  /* Whether this extension is fit for somebody else to run, the checks answerable from its files alone. Read
95
96
  * on demand rather than carried on the list: it reads the bundle off disk per extension, and it is looked at
@@ -53,13 +53,14 @@ export const workspaceContract = {
53
53
  .output(WorkspaceTreeSchema),
54
54
  // Lazy-load one directory's children, the tree returns ignored dirs (node_modules, .git, …) without children,
55
55
  // and the client fetches them here on expand so a giant node_modules can't blow the tree walk's entry budget.
56
+ // Bounded depth is for consumers that need a small subtree as data rather than one explorer row at a time.
56
57
  children: oc
57
58
  .route({
58
59
  method: "GET",
59
60
  path: "/workspace/children",
60
- summary: "One folder's contents",
61
+ summary: "A bounded folder listing",
61
62
  description:
62
- "The entries directly inside a single folder. This is how you open a folder the full tree walk deliberately left closed, such as an installed-packages directory with a hundred thousand files in it.",
63
+ "The entries inside a folder as one flat list. Direct children are the default, which is how the explorer opens a folder the full tree walk left closed; callers that need a small subtree can ask for up to five levels without a request per directory.",
63
64
  })
64
65
  .input(WorkspaceChildrenQuerySchema)
65
66
  .output(WorkspaceChildrenSchema),
package/src/events.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { AgentProviderSchema, PermissionModeSchema } from "./schemas/agent.js";
2
+ import { AgentHarnessSchema, AgentProviderSchema, PermissionModeSchema } from "./schemas/agent.js";
3
3
  import { AgentSummarySchema, LandConflictSchema } from "./schemas/agents.js";
4
4
  import { RateLimitInfoSchema } from "./schemas/claude-gate.js";
5
5
  import { FastModeStateSchema } from "./schemas/fast-mode.js";
@@ -437,8 +437,24 @@ export const SessionTranscriptSchema = z.object({
437
437
  "The conversation, in order. Each block of the agent's prose is its own message with the tools that block introduced, which is what reproduces the way it actually unfolded.",
438
438
  ),
439
439
  });
440
+ /* THE RECORD A REOPENED TAB IS REBUILT FROM: the messages, plus what the session behind them is BOUND to.
441
+ *
442
+ * A provider session is minted on one runtime under one credential, and it resumes only there, so a client
443
+ * deciding whether its next message continues this conversation or starts a fresh one needs all four facts
444
+ * together. The client cannot derive the last three: its tab holds the picks the NEXT turn would use, which
445
+ * after a mid-chat switch are exactly the ones the session does not belong to. Stamping those onto the session
446
+ * is what made switching BACK to the account that minted it announce a fresh session and then retire a
447
+ * perfectly resumable one, spending the whole transcript again on a cold prompt cache.
448
+ *
449
+ * All optional, and absent together on a conversation that has no session to resume. */
440
450
  export const AgentTranscriptSchema = SessionTranscriptSchema.extend({
441
451
  sessionId: z.string().optional().describe("The provider session behind the last turn, when there is one."),
452
+ provider: AgentProviderSchema.optional().describe("Which provider minted that session."),
453
+ harness: AgentHarnessSchema.optional().describe("Which runtime minted it: a session resumes only on the loop that opened it."),
454
+ account: z
455
+ .string()
456
+ .optional()
457
+ .describe("Which stored account it belongs to, as the daemon resolved it. Absent when no stored account paid for the turn."),
442
458
  });
443
459
 
444
460
  /* WHAT A PUBLISHED CONVERSATION'S PAGE IS HANDED, the whole of it, baked into the page as one JSON block.
@@ -495,8 +511,8 @@ const PlanCardSchema = z.object({
495
511
  kind: z.literal("plan").describe("The agent has written a plan and is waiting for a yes."),
496
512
  requestId: z.string().describe("What to send back when you answer."),
497
513
  text: z.string().describe("The plan itself."),
498
- // Present when the plan text POINTS at a document instead of being one: the model wrote the real plan to a
499
- // file and summarised it here. Absent when the text already is the whole plan, which is the ordinary case.
514
+ // Present when the adjacent plan prose POINTS at a document instead of being one: the model wrote the real
515
+ // plan to a file and summarised it there. Absent when the text already is the whole plan.
500
516
  document: CardDocumentSchema.optional().describe("The write-up this plan refers to, when the plan itself is a pointer to one."),
501
517
  });
502
518
  const QuestionCardSchema = z.object({
@@ -522,7 +538,22 @@ export type ParkedCard = z.infer<typeof ParkedCardSchema>;
522
538
  // `parentToolUseId` tags frames produced inside a subagent (Task tool); `subagent`/`subagent_update` report the
523
539
  // subagent itself, keyed by the same tool_use id those tagged frames carry.
524
540
  export const AgentEventSchema = z.discriminatedUnion("kind", [
525
- z.object({ kind: z.literal("session"), sessionId: z.string() }),
541
+ /* THE SESSION THIS TURN IS RUNNING, and the credential it belongs to.
542
+ *
543
+ * `account` is the account the daemon RESOLVED for the turn, which is not always the one the request named:
544
+ * a turn that names none is given the connected account with the most headroom (agent/harness-credentials.ts),
545
+ * so "the client's pick" and "who is paying" are different questions and only the daemon can answer the
546
+ * second. It rides here because a session belongs to the credential that minted it — that pairing is what
547
+ * decides whether the next message resumes this session or opens a fresh one — and a client that stamped its
548
+ * own pick onto the session instead would announce a fresh session for the account that actually holds it.
549
+ *
550
+ * Absent when the turn ran on the container's env token or on a translator subscription, where there is no
551
+ * stored account to name. */
552
+ z.object({
553
+ kind: z.literal("session"),
554
+ sessionId: z.string(),
555
+ account: z.string().optional().describe("Which stored account this session belongs to, as the daemon resolved it for the turn."),
556
+ }),
526
557
  /* WHERE AN ISOLATED TURN IS STANDING: the conversation's worktree identity, its branch (agent/<id>) and
527
558
  * the ROOT repo's short base sha. First frame of the turn, before any provider frames, and again each time
528
559
  * the branch MOVES underneath it, which is why `base` names where the branch sits now rather than the
package/src/hostnames.ts CHANGED
@@ -70,12 +70,12 @@ export const CATCH_ALL = { service: "http_status:404" } as const;
70
70
  // for as long as the file does.
71
71
  //
72
72
  // A *label* is the first-DNS-label prefix before `-<sandboxId>` (`preview-<panel>` / `port-<slot>` /
73
- // `public-<slot>`), the unit the platform's /sandbox/preview-route mints, so one endpoint serves all three.
73
+ // `public-<slot>`), the unit reachability is expressed in, so one rule covers all three.
74
74
  export const previewLabel = (panel: string): string => `preview-${panel}`;
75
75
  export const portLabel = (slot: string): string => `port-${slot}`;
76
76
  export const publicLabel = (slot: string): string => `public-${slot}`;
77
77
 
78
- // The hostname a label resolves to, what the platform's /sandbox/preview-route mints from the label alone.
78
+ // The hostname a label resolves to. Nothing mints it: the edge parses the sandbox id back out of the name.
79
79
  export const labelHostname = (label: string, id: string, zone: string): string => `${label}-${id}.${zone}`;
80
80
  export const previewHostname = (panel: string, id: string, zone: string): string => labelHostname(previewLabel(panel), id, zone);
81
81
  export const portHostname = (slot: string, id: string, zone: string): string => labelHostname(portLabel(slot), id, zone);
@@ -113,8 +113,10 @@ export const INGRESS_TUNNEL_PATH = "/tunnel/v1";
113
113
  export const INGRESS_GRANT_HEADER = "x-intentic-grant";
114
114
 
115
115
  /* The env vocabulary, every lane (connect one-liner, compose file, hosted machine env) hands the same pair
116
- * down and the entrypoint/daemon read exactly these names. SANDBOX_PUBLIC_URL is unchanged from the zrok era
117
- * and stays beside them. */
116
+ * down and the DAEMON reads exactly these names the entrypoint reads neither, and that is the shape of the
117
+ * change: reachability stopped being something a shell script arranges before the daemon starts and became
118
+ * one outbound dial the daemon makes for itself. SANDBOX_PUBLIC_URL is unchanged from the fabric before this
119
+ * one and stays beside them. */
118
120
  export const ENV_INGRESS_URL = "INGRESS_URL";
119
121
  export const ENV_SANDBOX_GRANT = "SANDBOX_GRANT";
120
122