@intentic/sandbox-contract 1.230.3 → 1.231.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 (56) hide show
  1. package/README.md +5 -0
  2. package/dist/contracts/agent.contract.d.ts +42 -6
  3. package/dist/contracts/agent.contract.d.ts.map +1 -1
  4. package/dist/contracts/agents.contract.d.ts +6 -1
  5. package/dist/contracts/agents.contract.d.ts.map +1 -1
  6. package/dist/contracts/capabilities.contract.d.ts +293 -0
  7. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  8. package/dist/contracts/capabilities.contract.js +10 -1
  9. package/dist/contracts/capabilities.contract.js.map +1 -1
  10. package/dist/contracts/extensions.contract.d.ts +15 -0
  11. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  12. package/dist/contracts/git.contract.d.ts +12 -2
  13. package/dist/contracts/git.contract.d.ts.map +1 -1
  14. package/dist/contracts/history.contract.d.ts +6 -1
  15. package/dist/contracts/history.contract.d.ts.map +1 -1
  16. package/dist/contracts/panels.contract.d.ts +1 -0
  17. package/dist/contracts/panels.contract.d.ts.map +1 -1
  18. package/dist/contracts/runner.contract.d.ts +811 -0
  19. package/dist/contracts/runner.contract.d.ts.map +1 -0
  20. package/dist/contracts/runner.contract.js +12 -0
  21. package/dist/contracts/runner.contract.js.map +1 -0
  22. package/dist/contracts/system.contract.d.ts +2 -3
  23. package/dist/contracts/system.contract.d.ts.map +1 -1
  24. package/dist/documents.d.ts +7 -0
  25. package/dist/documents.d.ts.map +1 -0
  26. package/dist/documents.js +27 -0
  27. package/dist/documents.js.map +1 -0
  28. package/dist/events.d.ts +58 -6
  29. package/dist/events.d.ts.map +1 -1
  30. package/dist/events.js +12 -1
  31. package/dist/events.js.map +1 -1
  32. package/dist/history-state.d.ts.map +1 -1
  33. package/dist/history-state.js +3 -0
  34. package/dist/history-state.js.map +1 -1
  35. package/dist/index.d.ts +380 -13
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +3 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/runner-protocol.d.ts +134 -0
  40. package/dist/runner-protocol.d.ts.map +1 -0
  41. package/dist/runner-protocol.js +95 -0
  42. package/dist/runner-protocol.js.map +1 -0
  43. package/dist/schemas.d.ts +62 -9
  44. package/dist/schemas.d.ts.map +1 -1
  45. package/dist/schemas.js +30 -11
  46. package/dist/schemas.js.map +1 -1
  47. package/package.json +4 -4
  48. package/src/contracts/capabilities.contract.ts +16 -0
  49. package/src/contracts/runner.contract.ts +33 -0
  50. package/src/documents.test.ts +66 -0
  51. package/src/documents.ts +71 -0
  52. package/src/events.ts +44 -2
  53. package/src/history-state.ts +7 -0
  54. package/src/index.ts +4 -0
  55. package/src/runner-protocol.ts +209 -0
  56. package/src/schemas.ts +106 -37
package/src/schemas.ts CHANGED
@@ -3,6 +3,7 @@ import { ExtensionManifestSchema } from "@intentic/extension-manifest";
3
3
  import { RegistryEntrySchema } from "@intentic/registry";
4
4
  import { z } from "zod";
5
5
  import { OutputFieldsSchema } from "./output-fields.js";
6
+ import { AgentPlacementSchema } from "./runner-protocol.js";
6
7
 
7
8
  // All request/response wire schemas for the sandbox daemon. Inputs that carry a `{param}` in their route path
8
9
  // (repo / id / name) merge the path param into the same flat object, oRPC fills the path placeholder from the
@@ -253,6 +254,13 @@ export const AgentTurnSchema = z
253
254
  .describe(
254
255
  "Work in this conversation's own private copy of the repos rather than the shared tree, so several agents can work at once. Needs a conversation id.",
255
256
  ),
257
+ /* WHERE the conversation executes, decided like `isolated` directly above: the request's choice on the
258
+ * first turn, the registry entry's on every turn after. `runner` implies isolation (the branch is what
259
+ * moves between machines) and needs a conversation id for the same reason `isolated` does. Absent =
260
+ * local. Design: docs/remote-runners-plan.md; refused until runners ship. */
261
+ placement: AgentPlacementSchema.optional().describe(
262
+ "Where this conversation runs: this sandbox (leave it out), or a paired runner by id. Decided on the first turn; later turns follow the conversation.",
263
+ ),
256
264
  /* Pin a NEW isolated conversation's worktree composition to these repository commits. Daemon-owned:
257
265
  * ordinary chats omit it and keep rebasing onto the current workspace; a workflow supplies the one
258
266
  * snapshot all of its candidates must share. Repeated iterations carry it too, which suppresses the
@@ -2778,7 +2786,13 @@ export const SandboxSettingsSchema = z.object({
2778
2786
  * `<provider>.<type>` ("discord.message.send") with `<provider>.*` as the per-provider wildcard; exact key
2779
2787
  * wins. An action with no rule is allowed, the empty default wires no hook at all, so an unconfigured
2780
2788
  * workspace pays nothing. "hold" cannot park a running turn (nobody may be there to answer); it refuses the
2781
- * live call and points the agent at the drafts outbox, which IS the held form of a send. */
2789
+ * live call and points the agent at the drafts outbox, which IS the held form of a send.
2790
+ *
2791
+ * The CHILD-AGENT surface reads the same book: `agents.spawn` covers starting, steering and answering
2792
+ * child agents on every provider, `agents.spawn.<provider>` singles one out (the specific key wins), and
2793
+ * the daemon's own taint floor holds a spawn from a turn that has taken in outside content unless the
2794
+ * owner wrote an explicit allow (guard/actions.ts childSpawn). "hold" refuses with the owner named, the
2795
+ * same translation a send gets. */
2782
2796
  actionRules: z
2783
2797
  .record(z.string(), AdmissionRuleSchema)
2784
2798
  .default({})
@@ -3812,13 +3826,43 @@ export const SnapshotFileDiffQuerySchema = z.object({
3812
3826
  scope: z.string().min(1).describe("Which part of the workspace the path belongs to."),
3813
3827
  path: z.string().min(1).describe("The file, relative to that scope."),
3814
3828
  });
3829
+ /* WHAT A FILE TOO BIG TO SEND WHOLE ANSWERS WITH INSTEAD, and why that is not simply "no".
3830
+ *
3831
+ * Both whole sides of a half-megabyte file are a megabyte of JSON per click, so above the cap they are not
3832
+ * sent, which used to be the end of it: the response said "too large" and every review surface printed one
3833
+ * sentence over an empty pane. That is the wrong trade, because the thing a reader wants out of a big file is
3834
+ * almost never the file: it is the handful of lines that MOVED, and those are small however big the file is.
3835
+ *
3836
+ * So the daemon diffs it and sends the CHANGED REGIONS as a unified patch, at the same three lines of context
3837
+ * a collapsed region keeps elsewhere. A 40 KB patch stands in for a 60 MB pair, and the reader gets the actual
3838
+ * review rather than a refusal. `patch` carries the `@@` sections only, the file headers git prints above them
3839
+ * name rev-specs no one can apply anyway.
3840
+ *
3841
+ * An added or deleted file has no counterpart to diff against, so its patch IS the file, one region of pure
3842
+ * +/− lines. That is still the right answer: cut to the budget, it is the head of the file, which is the peek
3843
+ * the reader came for.
3844
+ *
3845
+ * `patch` is absent only when there was nothing to make one from: a change too large even to render as a
3846
+ * patch, or a git that refused. The sizes are still there, so a surface can at least say how big the thing it
3847
+ * is not showing is. */
3848
+ export const PartialFileDiffSchema = z.object({
3849
+ beforeBytes: z.number().int().nonnegative().optional().describe("How big the before side is, in bytes. Absent when the file did not exist yet."),
3850
+ afterBytes: z.number().int().nonnegative().optional().describe("How big the after side is, in bytes. Absent when the file was deleted."),
3851
+ patch: z
3852
+ .string()
3853
+ .optional()
3854
+ .describe("The changed regions as unified-diff hunks (`@@` sections only). Absent when the change was too large to render even as a patch."),
3855
+ more: z.boolean().optional().describe("There were more changed regions than fit; the patch stops at a region boundary."),
3856
+ });
3857
+ export type PartialFileDiff = z.infer<typeof PartialFileDiffSchema>;
3858
+
3815
3859
  // Both sides of a file diff, a snapshot vs its parent, or a working tree vs HEAD; an absent side means the
3816
- // file was added/deleted. Binary or oversized content is flagged instead of shipped.
3860
+ // file was added/deleted. Binary content is flagged instead of shipped; oversized content arrives as `partial`.
3817
3861
  export const FileDiffSchema = z.object({
3818
- before: z.string().optional().describe("The whole file as it was. Absent when it did not exist yet."),
3819
- after: z.string().optional().describe("The whole file as it is now. Absent when it was deleted."),
3862
+ before: z.string().optional().describe("The whole file as it was. Absent when it did not exist yet, or when `partial` is set."),
3863
+ after: z.string().optional().describe("The whole file as it is now. Absent when it was deleted, or when `partial` is set."),
3820
3864
  binary: z.boolean().optional().describe("The file is not text, so neither side is sent."),
3821
- truncated: z.boolean().optional().describe("The file was too large to send whole, so what you have is the start of it."),
3865
+ partial: PartialFileDiffSchema.optional().describe("Set when the file was too large to send whole: what is sent instead of the two sides."),
3822
3866
  });
3823
3867
  export type FileDiff = z.infer<typeof FileDiffSchema>;
3824
3868
 
@@ -5617,6 +5661,26 @@ export const CapabilityOtpSchema = z.object({
5617
5661
  .describe("How long it lasts. Its expiring is what makes handing one to an agent safe, since the seed behind it is never revealed."),
5618
5662
  });
5619
5663
 
5664
+ /* POST /capabilities/probe response: did these settings actually reach the thing, asked BEFORE they are saved.
5665
+ *
5666
+ * The answer is a sentence rather than a status code because the reader is standing in front of a form: what
5667
+ * they need is either the service's own confirmation ("Reached GitHub, authenticated as ada") or the exact
5668
+ * refusal ("GitHub answered 401: the token is not valid"), in the place where the box they would fix still is.
5669
+ * That is also the whole point of doing it here: every one of these failures is otherwise discovered after the
5670
+ * add, on a card that says "not connected" with nothing about which of six answers was wrong.
5671
+ *
5672
+ * `ok: false` is a REPORTED failure, not a transport error: the probe ran and the service said no. A card whose
5673
+ * settings cannot be checked from here at all answers `checked: false`, which is a different thing from a
5674
+ * failure and must never be drawn as one. */
5675
+ export const CapabilityProbeSchema = z.object({
5676
+ checked: z.boolean().describe("Whether this connection can be tested from here at all. False is not a failure: it is 'no test exists'."),
5677
+ ok: z.boolean().describe("Whether the service answered as itself."),
5678
+ message: z
5679
+ .string()
5680
+ .describe("What happened, in the words a person standing in front of the form needs: the service's own answer, or its refusal."),
5681
+ });
5682
+ export type CapabilityProbe = z.infer<typeof CapabilityProbeSchema>;
5683
+
5620
5684
  // ---- hosts: the user's own connected computers (the `host` capability's live half) ----
5621
5685
  // The manifest says which machines the user INTENDS to have connected; this says which are actually holding a
5622
5686
  // socket right now. Nothing here is remembered across a daemon restart except the enrollment itself: a machine
@@ -7625,6 +7689,9 @@ export const PanelSummarySchema = z.object({
7625
7689
  servers: z
7626
7690
  .array(
7627
7691
  z.object({
7692
+ // The port itself, not just the URL it appears in: forwarding one is how a repo answering on
7693
+ // several ports becomes previewable at all, and that call takes a number.
7694
+ port: z.number().describe("The port it is listening on, which is what forwarding it takes."),
7628
7695
  url: z.string().describe("Where it answers, with the right scheme: a server on its own certificate is served over https."),
7629
7696
  dir: z
7630
7697
  .string()
@@ -7641,8 +7708,15 @@ export const PanelSummarySchema = z.object({
7641
7708
  }),
7642
7709
  )
7643
7710
  .describe("Every server this repository is really serving, found by looking at what is listening. Empty when nothing answers."),
7644
- // https://preview-<repo>-<sandboxId>.<zone>; absent when the sandbox has no zone or connect token (loopback/tests).
7645
- previewUrl: z.string().optional().describe("Where to open it from outside. Absent on a sandbox with no outside address."),
7711
+ /* https://preview-<repo>-<sandboxId>.<zone>, and ONLY where that address actually serves this repo: absent
7712
+ * on a sandbox with no zone or connect token (loopback/tests), and absent whenever the preview proxy has
7713
+ * nothing to route it to — nothing running, still starting, or (the ordinary monorepo) several dev servers
7714
+ * on ports of their own, none of which one hostname can stand for. Present ⇒ safe to open or frame, which
7715
+ * is what stops a surface from showing a 502 as if it were the app. */
7716
+ previewUrl: z
7717
+ .string()
7718
+ .optional()
7719
+ .describe("Where to open it from outside, present only while that address really serves it. Absent on a sandbox with no outside address."),
7646
7720
  // The workspace role this repo dir occupies (the three fixed dirs); absent for extra clones.
7647
7721
  role: z
7648
7722
  .enum(["intent", "desired-state", "app"])
@@ -8323,28 +8397,28 @@ export const BrowserNameParamSchema = z.object({ name: z.string().describe("Whic
8323
8397
  * (another agent, working, that you did not start):
8324
8398
  * • `subagent`, the SDK's Agent/Task tool. The daemon learns of it from the SubagentStart/SubagentStop hooks
8325
8399
  * and the task_* stream messages, joined on `toolUseId`.
8326
- * • `codex` / `grok`, a CLI the agent drove from its own Bash (agent/delegation.ts). Detected in the Bash
8327
- * PreToolUse hook, bound to its thread/session id from the command's output.
8328
- *
8329
- * `id` IS THE SPAWNING TOOL CALL'S id, the Agent card's, or the Bash card's for a delegation. It is the one key
8330
- * every source already carries (the SDK's subagent meta, its task_* messages, and the `parentToolUseId` the
8331
- * client nests inner frames under), so nothing has to be correlated: a card links to its subagent with the id it
8332
- * already has, and the subagent points back at the card the same way. The ids the transcripts are actually READ
8333
- * with, the SDK's agent id, a Codex thread, an OpenCode session, stay daemon-side, because no surface asks a
8334
- * question they answer.
8335
- *
8336
- * WHAT A KIND CHANGES, and it is only ever the live view: a subagent has no process of its own to look at, so
8337
- * watching it means reading its transcript. A delegation runs in a tmux window, so it has both, `terminal`
8338
- * names it, and the card keeps its existing "Watch in terminal" beside the transcript door. */
8339
- export const SubagentKindSchema = z.enum(["subagent", "codex", "grok"]);
8400
+ * • `spawned`, a full agent the turn started through the daemon's own spawn door (children/children.ts), on
8401
+ * ANY connected provider, Cursor's Composer, Codex, Gemini, another Claude. The daemon runs the child
8402
+ * itself, so its whole life is reported by direct calls rather than reconstructed from hooks or stdout.
8403
+ *
8404
+ * `id` IS THE SPAWNING TOOL CALL'S id for an SDK child (the one key its meta file, its task messages and the
8405
+ * client's `parentToolUseId` nesting all carry), and the child's own conversation id for a `spawned` one (the
8406
+ * spawn door returns it, so both sides hold it). A card links to its subagent with the id it has, and the
8407
+ * subagent points back at the card the same way. The ids the transcripts are actually READ with, the SDK's
8408
+ * agent id, stay daemon-side, because no surface asks a question they answer.
8409
+ *
8410
+ * WHAT A KIND CHANGES, and it is only ever the live view: an SDK subagent has no process of its own to look
8411
+ * at, so watching it means reading its transcript; a spawned child is a conversation of its own, so its live
8412
+ * view is that conversation's stream. */
8413
+ export const SubagentKindSchema = z.enum(["subagent", "spawned"]);
8340
8414
  export type SubagentKind = z.infer<typeof SubagentKindSchema>;
8341
8415
 
8342
8416
  // running/pending/blocked are live; the rest are terminal. Deliberately the SDK's own task vocabulary
8343
8417
  // (SDKTaskUpdatedMessage.patch.status) rather than AgentStatus: this is not a fleet card's lifecycle (no
8344
8418
  // draft/landed/conflict), and mapping the two would invent states neither side reports. `blocked` is the one
8345
- // addition the SDK never says: it comes from a delegated CLI's own signals (a Codex PermissionRequest hook, an
8346
- // OpenCode permission ask, agent/delegation-signals.ts), and it exists because "the child needs an answer" is
8347
- // the one live state a parent or an operator acts on differently from "the child is working".
8419
+ // addition the SDK never says: a spawned child's own question/permission/plan card raises it
8420
+ // (children/children.ts), and it exists because "the child needs an answer" is the one live state a parent or
8421
+ // an operator acts on differently from "the child is working".
8348
8422
  export const SubagentStatusSchema = z.enum(["pending", "running", "blocked", "completed", "failed", "killed", "paused"]);
8349
8423
  export type SubagentStatus = z.infer<typeof SubagentStatusSchema>;
8350
8424
 
@@ -8352,20 +8426,23 @@ export const SubagentSessionSchema = z.object({
8352
8426
  id: z
8353
8427
  .string()
8354
8428
  .describe(
8355
- "The id of the tool call that started it, which every side already holds, so a card links to its helper with the id it has and the helper points back the same way.",
8429
+ "The id of the tool call that started it (an SDK child) or the child's own conversation id (a spawned one); either way both sides already hold it, so a card links to its helper with the id it has and the helper points back the same way.",
8356
8430
  ),
8357
8431
  kind: SubagentKindSchema.describe(
8358
- "What sort of helper: one the runtime spawned, or a separate tool the agent drove from a shell. It changes only how you watch it.",
8432
+ "What sort of helper: one the runtime's own Task tool spawned in-process, or a full agent the daemon started for the turn. It changes only how you watch it.",
8359
8433
  ),
8360
8434
  // The conversation whose turn spawned this, what the area groups its rows by, and the way back to the chat
8361
8435
  // the card lives in.
8362
8436
  conversationId: z.string().describe("The conversation whose turn started it, and the way back to the chat it belongs to."),
8363
- // What it is and what it was asked to do: the subagent type (`Explore`, `general-purpose`) or the delegated
8364
- // provider's model, and the caller's one-line description. The area's row and the card's title read as
8365
- // `Explore · Locate claimIndexer definition`.
8437
+ // What it is and what it was asked to do: the subagent type (`Explore`, `general-purpose`) or a spawned
8438
+ // child's provider label, and the caller's one-line description. The area's row and the card's title read
8439
+ // as `Explore · Locate claimIndexer definition`.
8366
8440
  agentType: z.string().optional().describe("What kind of helper it is."),
8367
8441
  description: z.string().optional().describe("What it was asked to do, in one line."),
8368
8442
  model: z.string().optional().describe("Which model it runs on."),
8443
+ // Which provider serves a `spawned` child (its AgentProvider id), so the row can wear the right logo. An
8444
+ // SDK subagent implies its own: it runs on its parent's provider.
8445
+ provider: z.string().optional().describe("Which provider serves it, for a helper spawned across providers."),
8369
8446
  // How deep in the spawn tree (1 = spawned by the turn itself). From the SDK's meta.json; a subagent may
8370
8447
  // itself delegate, and a flat list that cannot say so reads as though the turn started all of them.
8371
8448
  spawnDepth: z
@@ -8403,14 +8480,6 @@ export const SubagentSessionSchema = z.object({
8403
8480
  .optional()
8404
8481
  .describe("Its report: what it concluded, without opening its record. The question a finished helper gets read for."),
8405
8482
  error: z.string().optional().describe("Why it failed, when it did."),
8406
- // A delegation's live view: the tmux session its command runs in. Absent for an SDK subagent, which has no
8407
- // process of its own to attach to.
8408
- terminal: z
8409
- .string()
8410
- .optional()
8411
- .describe(
8412
- "The terminal its command runs in, when there is one. Absent for a helper with no process of its own, which is watched by reading its record instead.",
8413
- ),
8414
8483
  });
8415
8484
  export type SubagentSession = z.infer<typeof SubagentSessionSchema>;
8416
8485
  export const SubagentsListSchema = z.object({