@intentic/sandbox-contract 1.231.0 → 1.232.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 (42) hide show
  1. package/README.md +6 -0
  2. package/dist/contracts/agent.contract.d.ts +11 -0
  3. package/dist/contracts/agent.contract.d.ts.map +1 -1
  4. package/dist/contracts/agent.contract.js +10 -1
  5. package/dist/contracts/agent.contract.js.map +1 -1
  6. package/dist/contracts/host.contract.d.ts +4 -0
  7. package/dist/contracts/host.contract.d.ts.map +1 -1
  8. package/dist/contracts/runner.contract.d.ts +106 -41
  9. package/dist/contracts/runner.contract.d.ts.map +1 -1
  10. package/dist/contracts/runner.contract.js +6 -1
  11. package/dist/contracts/runner.contract.js.map +1 -1
  12. package/dist/contracts/system.contract.d.ts +4 -0
  13. package/dist/contracts/system.contract.d.ts.map +1 -1
  14. package/dist/definition.d.ts +961 -0
  15. package/dist/definition.d.ts.map +1 -0
  16. package/dist/definition.js +71 -0
  17. package/dist/definition.js.map +1 -0
  18. package/dist/events.d.ts +8 -0
  19. package/dist/events.d.ts.map +1 -1
  20. package/dist/events.js +5 -0
  21. package/dist/events.js.map +1 -1
  22. package/dist/index.d.ts +16 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/schemas.d.ts +132 -23
  27. package/dist/schemas.d.ts.map +1 -1
  28. package/dist/schemas.js +50 -14
  29. package/dist/schemas.js.map +1 -1
  30. package/dist/workspace-state.d.ts +9 -0
  31. package/dist/workspace-state.d.ts.map +1 -1
  32. package/dist/workspace-state.js +2 -0
  33. package/dist/workspace-state.js.map +1 -1
  34. package/package.json +6 -6
  35. package/src/contracts/agent.contract.ts +19 -0
  36. package/src/contracts/runner.contract.ts +17 -1
  37. package/src/definition.ts +171 -0
  38. package/src/events.ts +44 -0
  39. package/src/index.ts +1 -0
  40. package/src/schemas.ts +119 -28
  41. package/src/workspace-state.test.ts +4 -0
  42. package/src/workspace-state.ts +19 -0
package/src/schemas.ts CHANGED
@@ -1937,6 +1937,23 @@ export const SteerSchema = z
1937
1937
  // /agent fetch (which sends no cancel frame).
1938
1938
  export const StopTurnSchema = z.object({ conversationId: z.string().min(1).describe("Which conversation's running turn to cancel.") });
1939
1939
 
1940
+ /* RUN THE HELD TURN AGAIN, and it carries a conversation id and NOTHING else, which is the entire point of it
1941
+ * existing as its own route rather than as a flag on a turn.
1942
+ *
1943
+ * A spent allowance leaves a turn stranded that the daemon still holds in full: the prompt, the attachments, the
1944
+ * model, the effort, the mode, the worktree, the session that holds whatever it managed to do. Every one of
1945
+ * those is on the turn the daemon already has, and a client that re-derived them from its own transcript would
1946
+ * be re-deriving them from the STRIPPED copy it renders (no preamble notes, no attachment note, no model), which
1947
+ * is how a re-send comes to run a different turn from the one it claims to repeat.
1948
+ *
1949
+ * So the caller says only WHICH conversation, and the daemon re-runs the turn it kept. What comes back is an
1950
+ * ordinary StartedTurn, and the caller then attaches to it exactly as it would to a turn somebody else started
1951
+ * (the resume note on the prompt is what tells an attaching window to reuse the bubble that is already there
1952
+ * instead of drawing the same message twice). */
1953
+ export const ResumeTurnSchema = z.object({
1954
+ conversationId: z.string().min(1).describe("Which conversation's held turn to run again."),
1955
+ });
1956
+
1940
1957
  // ---- claude rate-limit gate ----
1941
1958
  // The GATE signal: whether the provider is letting turns through right now, and, when it is refusing, which
1942
1959
  // window is binding and when it lifts. This is the SDK's rate_limit_event, mapped one-to-one, and it is only
@@ -7890,15 +7907,41 @@ export type MachineSandbox = z.infer<typeof MachineSandboxSchema>;
7890
7907
  *
7891
7908
  * The machine enforces which of them it will do: `sandboxes` covers everything but removal, which takes its own
7892
7909
  * switch, and a refusal comes back as the machine's own sentence naming the control to flip. */
7893
- export const MachineSandboxOpSchema = z.enum(["start", "stop", "restart", "prepare", "update", "rebuild", "rollback", "remove", "logs"]);
7910
+ /* `runner-up` / `runner-remove` are the same door for a container that belongs to THIS SANDBOX rather than to
7911
+ * a person: a runner (runners/, docs/remote-runners-plan.md at the workspace root). They ride here because to
7912
+ * the machine they are the same act it already does, run and remove a sandbox container, and to the person
7913
+ * clicking they are the same row of buttons. Both take the `sandboxes` switch and neither takes the removal
7914
+ * one: a runner holds no workspace of its own, only a mirror of the parent's git, so removing it destroys
7915
+ * nothing the parent does not still have. */
7916
+ export const MachineSandboxOpSchema = z.enum([
7917
+ "start",
7918
+ "stop",
7919
+ "restart",
7920
+ "prepare",
7921
+ "update",
7922
+ "rebuild",
7923
+ "rollback",
7924
+ "remove",
7925
+ "logs",
7926
+ "runner-up",
7927
+ "runner-remove",
7928
+ ]);
7894
7929
  export type MachineSandboxOp = z.infer<typeof MachineSandboxOpSchema>;
7895
7930
 
7896
7931
  export const MachineSandboxFlowSchema = z.object({
7897
7932
  op: MachineSandboxOpSchema,
7933
+ // Which sandbox, or, for the two runner ops, which RUNNER: the name it is known by at both ends, the
7934
+ // parent's `/system/runners` list and the machine's `ic runner list`.
7898
7935
  slug: z.string().min(1),
7899
7936
  // The approved overlay's sha256, required by `rebuild` and meaningless to the rest. It is the trust anchor:
7900
7937
  // only content that still hashes to what the owner reviewed is ever built.
7901
7938
  hash: z.string().optional(),
7939
+ /* `runner-up` only, and both are filled in by the DAEMON, never by the caller: where the runner dials
7940
+ * (this sandbox's public URL) and the single-use pairing it redeems there. The browser asks for a runner
7941
+ * on a machine; it never holds the credential that makes one, which is what keeps a pairing out of every
7942
+ * surface between here and that machine. */
7943
+ parentUrl: z.string().optional(),
7944
+ pair: z.string().optional().meta({ secret: true }),
7902
7945
  });
7903
7946
  export type MachineSandboxFlow = z.infer<typeof MachineSandboxFlowSchema>;
7904
7947
 
@@ -8500,6 +8543,73 @@ export const SubagentIdParamSchema = z.object({ id: z.string() });
8500
8543
  // proposal present with a hash different from custom's.
8501
8544
 
8502
8545
  const environmentFileSchema = z.object({ content: z.string(), hash: z.string() });
8546
+
8547
+ /* ---- environment DRIFT: what the live container has that the image did not put there ----
8548
+ *
8549
+ * Anything installed outside /work dies with the container, and transcript mining showed the same tools being
8550
+ * reinstalled session after session (cargo-xwin six times, a Windows rustup target eight) before anyone thought
8551
+ * to bake them. Drift is the daemon OBSERVING that gap rather than trusting the model to report it: apt installs
8552
+ * read from dpkg's own log, everything else from system paths newer than the container itself. Two channels
8553
+ * because they are disjoint by construction — dpkg unpacks files with their archive mtimes, so an mtime sweep
8554
+ * cannot see apt, and nothing apt does lands under the swept prefixes' hand-installed corners. */
8555
+ export const EnvironmentDriftSchema = z.object({
8556
+ // When this container was created (PID 1's start). A snapshot whose bornAt is not the running container's
8557
+ // describes a container that no longer exists, and every reader must treat it as no drift at all.
8558
+ bornAt: z.number(),
8559
+ // When the probe ran.
8560
+ at: z.number(),
8561
+ // Debian packages installed since the container was born, from /var/log/dpkg.log.
8562
+ apt: z.array(z.string()),
8563
+ // System paths (outside /work) newer than the container, collapsed so a browser download is one entry.
8564
+ paths: z.array(z.string()),
8565
+ });
8566
+ export type EnvironmentDrift = z.infer<typeof EnvironmentDriftSchema>;
8567
+
8568
+ // How a runtime install was made, which decides whether the daemon can draft a Dockerfile step for it
8569
+ // mechanically (apt/cargo/npm/rustup-target) or only surface it for a person to route (pip belongs in a venv or
8570
+ // a Debian package, "other" is a curl|sh whose replay could embed anything).
8571
+ export const RuntimeInstallKindSchema = z.enum(["apt", "pip", "cargo", "npm", "rustup-target", "playwright", "gem", "pipx", "go", "other"]);
8572
+ export type RuntimeInstallKind = z.infer<typeof RuntimeInstallKindSchema>;
8573
+
8574
+ /* One tool's runtime-install history across sessions: the ledger entry behind the recurrence signal. Sessions
8575
+ * are the unit of recurrence — a session that retries an install five times needed it once — and the entry
8576
+ * survives container recreates (the file lives under /work), which is exactly what makes "installed again in a
8577
+ * fresh container" observable at all. */
8578
+ export const RuntimeInstallSchema = z.object({
8579
+ tool: z.string(),
8580
+ kind: RuntimeInstallKindSchema,
8581
+ // Distinct conversation ids that installed it, capped; length is the recurrence count that gates drafting.
8582
+ sessions: z.array(z.string()),
8583
+ // The most recent install commands, capped, secrets already masked to references by the harness.
8584
+ commands: z.array(z.string()),
8585
+ firstAt: z.number(),
8586
+ lastAt: z.number(),
8587
+ count: z.number(),
8588
+ // The owner rejected an auto-drafted step for this tool: never propose it again until this is cleared.
8589
+ declinedAt: z.number().optional(),
8590
+ });
8591
+ export type RuntimeInstall = z.infer<typeof RuntimeInstallSchema>;
8592
+
8593
+ export const RuntimeInstallsFileSchema = z.object({
8594
+ installs: z.array(RuntimeInstallSchema),
8595
+ // The last drift snapshot, persisted so a daemon restart does not blank the card until the next sweep.
8596
+ drift: EnvironmentDriftSchema.optional(),
8597
+ });
8598
+ export type RuntimeInstallsFile = z.infer<typeof RuntimeInstallsFileSchema>;
8599
+
8600
+ // A ledger entry as the Environment card shows it: recurrence joined with whether the install is present in the
8601
+ // LIVE container (drift-corroborated), already drafted for approval, or previously declined.
8602
+ export const EnvironmentRecurringSchema = z.object({
8603
+ tool: z.string(),
8604
+ kind: RuntimeInstallKindSchema,
8605
+ sessions: z.number(),
8606
+ lastAt: z.number(),
8607
+ live: z.boolean(),
8608
+ drafted: z.boolean().optional(),
8609
+ declined: z.boolean().optional(),
8610
+ });
8611
+ export type EnvironmentRecurring = z.infer<typeof EnvironmentRecurringSchema>;
8612
+
8503
8613
  export const EnvironmentSchema = z.object({
8504
8614
  proposal: environmentFileSchema.optional(),
8505
8615
  // The owner-approved agent-written custom section (.intentic/config/environment.custom.Dockerfile).
@@ -8509,6 +8619,10 @@ export const EnvironmentSchema = z.object({
8509
8619
  appliedHash: z.string().optional(),
8510
8620
  // config.sandbox.name, the UI derives the rebuild one-liner's slug from it.
8511
8621
  container: z.string().optional(),
8622
+ // What the live container has that the image did not put there; absent until the first sweep of this container.
8623
+ drift: EnvironmentDriftSchema.optional(),
8624
+ // Runtime installs worth the owner's attention: recurring across sessions, or present-and-doomed right now.
8625
+ recurring: z.array(EnvironmentRecurringSchema).optional(),
8512
8626
  });
8513
8627
  export type Environment = z.infer<typeof EnvironmentSchema>;
8514
8628
  export const EnvironmentApproveSchema = z.object({ hash: z.string().min(1) });
@@ -8577,33 +8691,10 @@ export type EnvironmentContents = z.infer<typeof EnvironmentContentsSchema>;
8577
8691
  * by entry in WORKSPACE_STATE_FILES / HISTORY_STATE_FILES. It cannot carry the other two, and the honest
8578
8692
  * consequence is that an import ends in a REPORT rather than a claim of equivalence, the container has no
8579
8693
  * docker socket, so only the host can rebuild the image the overlay describes.
8580
- */
8581
-
8582
- // What the bundle says about itself, written as its first tar entry so a reader learns the shape before the
8583
- // bytes. `secrets` is the owner's export-time choice; the restorer re-derives every decision from the manifests
8584
- // rather than trusting this, and uses it only to explain what is missing.
8585
- export const BundleManifestSchema = z.object({
8586
- // Bumped when the layout changes in a way an older daemon would misread. Refused rather than guessed at.
8587
- version: z.literal(1),
8588
- // Where it came from, for the report's first line. Never used to authorize anything.
8589
- sandbox: z.object({ name: z.string() }).optional(),
8590
- createdAt: z.number(),
8591
- secrets: z.boolean(),
8592
- /* The environment the target has to reproduce, carried as FACTS rather than as the composed file (which the
8593
- * target recomposes against its OWN base image on first boot). `customDockerfile` is the owner-approved
8594
- * source section; `capabilities` names what contributed the remaining fragments, so the report can list what
8595
- * to re-add when the configs themselves did not travel. */
8596
- environment: z.object({
8597
- customDockerfile: z.string().optional(),
8598
- baseImage: z.string().optional(),
8599
- approvedHash: z.string().optional(),
8600
- capabilities: z.array(z.object({ id: z.string(), kind: z.string() })),
8601
- }),
8602
- // Every path class the bundle deliberately left out, with the manifest's own note where it has one. This is
8603
- // what turns "the export skipped things" from a silence into a list the owner can act on.
8604
- excluded: z.array(z.object({ path: z.string(), portability: z.string(), note: z.string().optional() })),
8605
- });
8606
- export type BundleManifest = z.infer<typeof BundleManifestSchema>;
8694
+ *
8695
+ * The bundle's manifest (BundleManifestSchema) lives in definition.ts beside the sandbox DEFINITION it embeds:
8696
+ * a bundle is definition + state, and keeping the two schemas together is what keeps the two export doors from
8697
+ * drifting into different answers about what an environment is. */
8607
8698
 
8608
8699
  // What a restore actually did. `needsAction` is the part that matters: the environment rebuild command, the
8609
8700
  // credentials to re-enter, the logins to redo, each one a thing the target cannot do for itself.
@@ -343,6 +343,10 @@ describe(`VERSIONED_STATE_PATHS`, () => {
343
343
  // The owner's per-extension update posture (notify / agent / auto): a standing decision about
344
344
  // what may run unattended, which is exactly the kind of edit worth a line in `git log`.
345
345
  `.intentic/config/extension-update-policy.json`,
346
+ // Which commands are heavy enough to take turns, and how many may run at once. Tracked because
347
+ // raising that limit is a decision about every session sharing the box, and `git log` is the only
348
+ // thing that answers "since when have we been allowing four of these at a time".
349
+ `.intentic/config/heavy-commands.json`,
346
350
  `.intentic/config/loop-designs.json`,
347
351
  `.intentic/config/personas.json`,
348
352
  // A persona's own kit: the prompt it runs on and the skills only its turns reach. Tracked for the
@@ -221,6 +221,18 @@ const STATE_FILES = [
221
221
  },
222
222
 
223
223
  { path: ".intentic/config/settings.json", invalidates: ["settings", "manifests"], portability: "carry", versioned: true },
224
+ /* Which agent commands are heavy enough to take turns, and how many may run at once (the daemon reads it
225
+ * per Bash command: platform/heavy-commands.ts).
226
+ *
227
+ * `carry`, because the answer is a property of the WORKSPACE rather than of this machine: `pnpm test` fans
228
+ * out to the same 74 packages wherever the repo is cloned, so a fresh sandbox should arrive already knowing
229
+ * which commands to queue rather than rediscovering it by freezing once.
230
+ *
231
+ * `versioned` for the reason the config slice generally is (personas.json's entry argues it): the file
232
+ * changes at human speed, it holds no secret, and a change to it is exactly the kind a reviewer should see
233
+ * — raising the limit is a decision about everyone's sessions on that box, and `git log` is the only thing
234
+ * that answers "since when have we allowed four of these at once". */
235
+ { path: ".intentic/config/heavy-commands.json", invalidates: ["settings"], portability: "carry", versioned: true },
224
236
  // The rule table's last-fired stamps, beside the rules themselves. `derived` rather than `carry`: it is a
225
237
  // record of what happened in THIS sandbox, and carrying it to a fresh one would date every rule to work
226
238
  // that machine never did.
@@ -230,6 +242,13 @@ const STATE_FILES = [
230
242
  portability: "derived",
231
243
  note: "Stamps of when each rule last did something; the new sandbox starts its own record.",
232
244
  },
245
+ /* The runtime-install ledger: which tools sessions installed into the container at runtime, how often, and
246
+ * the last drift snapshot (environment/runtime-installs.ts). `carry` where rule-firings chose `derived`,
247
+ * because the two record different subjects: a firing is about what THIS machine did, while the ledger is
248
+ * about what this WORKSPACE's tasks keep needing — a workspace moved to a fresh sandbox will hit the same
249
+ * missing tools, and arriving with the recurrence memory is the whole reason it is kept. The drift snapshot
250
+ * inside is machine-scoped, and self-expires on the move: its bornAt can never match the new container. */
251
+ { path: ".intentic/records/runtime-installs.json", invalidates: ["environment"], portability: "carry" },
233
252
  /* Written by the AGENT's file tools (the drafts skill), read by the owner's approval inbox, the one entry
234
253
  * here whose whole point is that a change arrives from outside the browser that renders it. `authored`:
235
254
  * a draft is text somebody wrote, and "find the reddit draft about X" is an ordinary search.