@tanstack/ai-sandbox 0.2.4 → 0.3.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 (158) hide show
  1. package/dist/esm/agents-file.js +53 -34
  2. package/dist/esm/agents-file.js.map +1 -1
  3. package/dist/esm/align.d.ts +121 -0
  4. package/dist/esm/align.js +197 -0
  5. package/dist/esm/align.js.map +1 -0
  6. package/dist/esm/approvals.js +63 -29
  7. package/dist/esm/approvals.js.map +1 -1
  8. package/dist/esm/attach-preflight.d.ts +85 -0
  9. package/dist/esm/attach-preflight.js +189 -0
  10. package/dist/esm/attach-preflight.js.map +1 -0
  11. package/dist/esm/bootstrap.js +103 -117
  12. package/dist/esm/bootstrap.js.map +1 -1
  13. package/dist/esm/bridge-events.js +96 -71
  14. package/dist/esm/bridge-events.js.map +1 -1
  15. package/dist/esm/capabilities.d.ts +0 -5
  16. package/dist/esm/capabilities.js +32 -28
  17. package/dist/esm/capabilities.js.map +1 -1
  18. package/dist/esm/chunk-identity.d.ts +52 -0
  19. package/dist/esm/chunk-identity.js +102 -0
  20. package/dist/esm/chunk-identity.js.map +1 -0
  21. package/dist/esm/claim.d.ts +187 -0
  22. package/dist/esm/claim.js +349 -0
  23. package/dist/esm/claim.js.map +1 -0
  24. package/dist/esm/contracts.d.ts +13 -0
  25. package/dist/esm/driver.d.ts +83 -0
  26. package/dist/esm/driver.js +138 -0
  27. package/dist/esm/driver.js.map +1 -0
  28. package/dist/esm/durability.d.ts +263 -0
  29. package/dist/esm/durability.js +230 -0
  30. package/dist/esm/durability.js.map +1 -0
  31. package/dist/esm/errors.js +28 -24
  32. package/dist/esm/errors.js.map +1 -1
  33. package/dist/esm/file-diff.js +151 -135
  34. package/dist/esm/file-diff.js.map +1 -1
  35. package/dist/esm/git-exec.js +51 -62
  36. package/dist/esm/git-exec.js.map +1 -1
  37. package/dist/esm/harness-cwd.js +24 -19
  38. package/dist/esm/harness-cwd.js.map +1 -1
  39. package/dist/esm/index.d.ts +30 -8
  40. package/dist/esm/index.js +23 -91
  41. package/dist/esm/instance-store.d.ts +88 -0
  42. package/dist/esm/instance-store.js +67 -0
  43. package/dist/esm/instance-store.js.map +1 -0
  44. package/dist/esm/journal-bytes.d.ts +67 -0
  45. package/dist/esm/journal-bytes.js +110 -0
  46. package/dist/esm/journal-bytes.js.map +1 -0
  47. package/dist/esm/journal-reader.d.ts +66 -0
  48. package/dist/esm/journal-reader.js +228 -0
  49. package/dist/esm/journal-reader.js.map +1 -0
  50. package/dist/esm/journal-sweep.d.ts +113 -0
  51. package/dist/esm/journal-sweep.js +309 -0
  52. package/dist/esm/journal-sweep.js.map +1 -0
  53. package/dist/esm/journal.d.ts +542 -0
  54. package/dist/esm/journal.js +679 -0
  55. package/dist/esm/journal.js.map +1 -0
  56. package/dist/esm/key.js +36 -33
  57. package/dist/esm/key.js.map +1 -1
  58. package/dist/esm/middleware.d.ts +50 -2
  59. package/dist/esm/middleware.js +335 -208
  60. package/dist/esm/middleware.js.map +1 -1
  61. package/dist/esm/ngrok.js +75 -49
  62. package/dist/esm/ngrok.js.map +1 -1
  63. package/dist/esm/policy.js +43 -34
  64. package/dist/esm/policy.js.map +1 -1
  65. package/dist/esm/projection.js +16 -8
  66. package/dist/esm/projection.js.map +1 -1
  67. package/dist/esm/reap.d.ts +238 -0
  68. package/dist/esm/reap.js +355 -0
  69. package/dist/esm/reap.js.map +1 -0
  70. package/dist/esm/reclaim.d.ts +84 -0
  71. package/dist/esm/reclaim.js +106 -0
  72. package/dist/esm/reclaim.js.map +1 -0
  73. package/dist/esm/remote-tools.js +73 -62
  74. package/dist/esm/remote-tools.js.map +1 -1
  75. package/dist/esm/run.d.ts +93 -25
  76. package/dist/esm/run.js +274 -79
  77. package/dist/esm/run.js.map +1 -1
  78. package/dist/esm/runner.d.ts +119 -2
  79. package/dist/esm/runner.js +270 -51
  80. package/dist/esm/runner.js.map +1 -1
  81. package/dist/esm/sandbox.d.ts +3 -2
  82. package/dist/esm/sandbox.js +139 -123
  83. package/dist/esm/sandbox.js.map +1 -1
  84. package/dist/esm/secrets.js +39 -47
  85. package/dist/esm/secrets.js.map +1 -1
  86. package/dist/esm/setup-plan.js +22 -14
  87. package/dist/esm/setup-plan.js.map +1 -1
  88. package/dist/esm/shell.d.ts +8 -0
  89. package/dist/esm/shell.js +197 -158
  90. package/dist/esm/shell.js.map +1 -1
  91. package/dist/esm/testkit/conformance.d.ts +16 -0
  92. package/dist/esm/testkit/conformance.js +97 -0
  93. package/dist/esm/testkit/conformance.js.map +1 -0
  94. package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
  95. package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
  96. package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
  97. package/dist/esm/testkit/journal-conformance.d.ts +51 -0
  98. package/dist/esm/testkit/journal-conformance.js +378 -0
  99. package/dist/esm/testkit/journal-conformance.js.map +1 -0
  100. package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
  101. package/dist/esm/testkit/reaper-conformance.js +847 -0
  102. package/dist/esm/testkit/reaper-conformance.js.map +1 -0
  103. package/dist/esm/testkit/shell-spawn.d.ts +2 -0
  104. package/dist/esm/testkit/shell-spawn.js +60 -0
  105. package/dist/esm/testkit/shell-spawn.js.map +1 -0
  106. package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
  107. package/dist/esm/testkit/takeover-conformance.js +685 -0
  108. package/dist/esm/testkit/takeover-conformance.js.map +1 -0
  109. package/dist/esm/tool-bridge.js +227 -180
  110. package/dist/esm/tool-bridge.js.map +1 -1
  111. package/dist/esm/tool-history.d.ts +62 -0
  112. package/dist/esm/tool-history.js +171 -0
  113. package/dist/esm/tool-history.js.map +1 -0
  114. package/dist/esm/watch.js +310 -236
  115. package/dist/esm/watch.js.map +1 -1
  116. package/dist/esm/workspace.d.ts +1 -1
  117. package/dist/esm/workspace.js +49 -28
  118. package/dist/esm/workspace.js.map +1 -1
  119. package/package.json +16 -6
  120. package/skills/ai-sandbox/SKILL.md +658 -20
  121. package/src/align.ts +297 -0
  122. package/src/attach-preflight.ts +292 -0
  123. package/src/capabilities.ts +4 -13
  124. package/src/chunk-identity.ts +154 -0
  125. package/src/claim.ts +479 -0
  126. package/src/contracts.ts +13 -0
  127. package/src/driver.ts +205 -0
  128. package/src/durability.ts +380 -0
  129. package/src/index.ts +212 -27
  130. package/src/instance-store.ts +122 -0
  131. package/src/journal-bytes.ts +136 -0
  132. package/src/journal-reader.ts +359 -0
  133. package/src/journal-sweep.ts +406 -0
  134. package/src/journal.ts +875 -0
  135. package/src/middleware.ts +470 -30
  136. package/src/reap.ts +723 -0
  137. package/src/reclaim.ts +191 -0
  138. package/src/run.ts +365 -75
  139. package/src/runner.ts +347 -3
  140. package/src/sandbox.ts +38 -8
  141. package/src/shell.ts +106 -38
  142. package/src/testkit/conformance.ts +117 -0
  143. package/src/testkit/durable-run-fields-conformance.ts +147 -0
  144. package/src/testkit/journal-conformance.ts +676 -0
  145. package/src/testkit/reaper-conformance.ts +1201 -0
  146. package/src/testkit/shell-spawn.ts +67 -0
  147. package/src/testkit/takeover-conformance.ts +1040 -0
  148. package/src/tool-history.ts +245 -0
  149. package/src/workspace.ts +1 -1
  150. package/dist/esm/index.js.map +0 -1
  151. package/dist/esm/run-log.d.ts +0 -81
  152. package/dist/esm/run-log.js +0 -107
  153. package/dist/esm/run-log.js.map +0 -1
  154. package/dist/esm/store.d.ts +0 -53
  155. package/dist/esm/store.js +0 -34
  156. package/dist/esm/store.js.map +0 -1
  157. package/src/run-log.ts +0 -224
  158. package/src/store.ts +0 -83
@@ -1,128 +1,144 @@
1
- import { bootstrapWorkspace } from "./bootstrap.js";
2
- import { resolveAllSecrets } from "./secrets.js";
1
+ import { InMemorySandboxInstanceStore } from "./instance-store.js";
3
2
  import { computeSandboxKey } from "./key.js";
4
- import { InMemorySandboxStore, InMemoryLockStore } from "./store.js";
3
+ import { resolveAllSecrets } from "./secrets.js";
4
+ import { bootstrapWorkspace } from "./bootstrap.js";
5
+ import { InMemoryLockStore } from "@tanstack/ai/locks";
6
+ //#region src/sandbox.ts
7
+ /**
8
+ * `defineSandbox()` returns a LAZY controller — it never creates a sandbox at
9
+ * definition time. `withSandbox()` (and advanced users) call `ensure()` to
10
+ * resume-or-create, following: provider.resume → provider.restoreSnapshot →
11
+ * create + bootstrap. The controller folds provider/workspace/policy/lifecycle
12
+ * into a stable instance key and coordinates through the (optional) lock +
13
+ * sandbox stores.
14
+ */
15
+ /**
16
+ * Parse a human-readable duration string into milliseconds.
17
+ * Supports `'<n>h'` (hours) and `'<n>m'` (minutes).
18
+ * Returns `undefined` when the input is undefined or the format is unrecognised.
19
+ */
5
20
  function parseMaxAgeMs(value) {
6
- if (value === void 0) return void 0;
7
- const hourMatch = /^(\d+)h$/.exec(value);
8
- if (hourMatch) return Number(hourMatch[1]) * 60 * 60 * 1e3;
9
- const minuteMatch = /^(\d+)m$/.exec(value);
10
- if (minuteMatch) return Number(minuteMatch[1]) * 60 * 1e3;
11
- return void 0;
21
+ if (value === void 0) return void 0;
22
+ const hourMatch = /^(\d+)h$/.exec(value);
23
+ if (hourMatch) return Number(hourMatch[1]) * 60 * 60 * 1e3;
24
+ const minuteMatch = /^(\d+)m$/.exec(value);
25
+ if (minuteMatch) return Number(minuteMatch[1]) * 60 * 1e3;
12
26
  }
13
- const fallbackStore = new InMemorySandboxStore();
14
- const fallbackLocks = new InMemoryLockStore();
27
+ /**
28
+ * Bound for the unfenced teardown `destroy` call (see `destroy` below). Long
29
+ * enough that a slow provider API still completes, short enough that a wedged
30
+ * one cannot pin the process forever.
31
+ */
32
+ var DESTROY_TIMEOUT_MS = 60 * 1e3;
33
+ var fallbackStore = new InMemorySandboxInstanceStore();
34
+ var fallbackLocks = new InMemoryLockStore();
15
35
  function defineSandbox(config) {
16
- const keyInputFor = (ctx) => ({
17
- threadId: config.lifecycle?.reuse === "none" ? `${ctx.threadId}:${ctx.runId}` : ctx.threadId,
18
- sandboxId: config.id,
19
- providerName: config.provider.name,
20
- workspace: config.workspace,
21
- tenant: ctx.tenant
22
- });
23
- const ensure = async (ctx) => {
24
- const store = ctx.store ?? fallbackStore;
25
- const locks = ctx.locks ?? fallbackLocks;
26
- const key = computeSandboxKey(keyInputFor(ctx));
27
- const caps = config.provider.capabilities();
28
- return locks.withLock(`sandbox:${key}`, async () => {
29
- const effectiveSnapshot = config.lifecycle?.snapshot ?? (caps.snapshots ? "after-setup" : "none");
30
- const maxAgeMs = parseMaxAgeMs(config.lifecycle?.snapshotMaxAge);
31
- const existing = await store.get(key);
32
- if (existing) {
33
- const tooOld = maxAgeMs !== void 0 && Date.now() - existing.updatedAt > maxAgeMs;
34
- if (!tooOld) {
35
- const resumed = await config.provider.resume({
36
- id: existing.providerSandboxId,
37
- signal: ctx.signal
38
- });
39
- if (resumed) {
40
- await store.upsert({
41
- ...existing,
42
- latestRunId: ctx.runId,
43
- updatedAt: Date.now()
44
- });
45
- return resumed;
46
- }
47
- if (existing.latestSnapshotId && caps.snapshots && config.provider.restoreSnapshot) {
48
- const restored = await config.provider.restoreSnapshot({
49
- snapshotId: existing.latestSnapshotId,
50
- workspace: config.workspace,
51
- policy: config.policy,
52
- env: config.workspace?.secrets !== void 0 ? resolveAllSecrets(config.workspace.secrets) : void 0,
53
- signal: ctx.signal
54
- });
55
- await store.upsert({
56
- ...existing,
57
- providerSandboxId: restored.id,
58
- latestRunId: ctx.runId,
59
- updatedAt: Date.now()
60
- });
61
- return restored;
62
- }
63
- }
64
- }
65
- const created = await config.provider.create({
66
- // Deterministic id so consumers can reconstruct the provider sandbox
67
- // address from run context (not just from the store record).
68
- id: key,
69
- workspace: config.workspace,
70
- policy: config.policy,
71
- env: config.workspace?.secrets !== void 0 ? resolveAllSecrets(config.workspace.secrets) : void 0,
72
- signal: ctx.signal
73
- });
74
- if (config.workspace) {
75
- try {
76
- await bootstrapWorkspace(created, config.workspace, {
77
- signal: ctx.signal
78
- });
79
- } catch (error) {
80
- await created.destroy().catch(() => {
81
- });
82
- throw error;
83
- }
84
- }
85
- let latestSnapshotId;
86
- if (effectiveSnapshot === "after-setup" && caps.snapshots && created.snapshot) {
87
- latestSnapshotId = (await created.snapshot("after-setup")).id;
88
- }
89
- await store.upsert({
90
- key,
91
- provider: config.provider.name,
92
- providerSandboxId: created.id,
93
- latestSnapshotId,
94
- threadId: ctx.threadId,
95
- latestRunId: ctx.runId,
96
- updatedAt: Date.now()
97
- });
98
- return created;
99
- });
100
- };
101
- const destroy = async (ctx) => {
102
- const store = ctx.store ?? fallbackStore;
103
- const key = computeSandboxKey(keyInputFor(ctx));
104
- const existing = await store.get(key);
105
- if (!existing) return;
106
- await config.provider.destroy({
107
- id: existing.providerSandboxId,
108
- signal: ctx.signal
109
- });
110
- await store.delete(key);
111
- };
112
- return {
113
- id: config.id,
114
- provider: config.provider,
115
- workspace: config.workspace,
116
- policy: config.policy,
117
- lifecycle: config.lifecycle,
118
- hooks: config.hooks,
119
- fileEvents: config.fileEvents,
120
- key: (ctx) => computeSandboxKey(keyInputFor(ctx)),
121
- ensure,
122
- destroy
123
- };
36
+ const keyInputFor = (ctx) => ({
37
+ threadId: config.lifecycle?.reuse === "none" ? `${ctx.threadId}:${ctx.runId}` : ctx.threadId,
38
+ sandboxId: config.id,
39
+ providerName: config.provider.name,
40
+ workspace: config.workspace,
41
+ tenant: ctx.tenant
42
+ });
43
+ const ensure = async (ctx) => {
44
+ const store = ctx.store ?? fallbackStore;
45
+ const locks = ctx.locks ?? fallbackLocks;
46
+ const key = computeSandboxKey(keyInputFor(ctx));
47
+ const caps = config.provider.capabilities();
48
+ return locks.withLock(`sandbox:${key}`, async () => {
49
+ const effectiveSnapshot = config.lifecycle?.snapshot ?? (caps.snapshots ? "after-setup" : "none");
50
+ const maxAgeMs = parseMaxAgeMs(config.lifecycle?.snapshotMaxAge);
51
+ const existing = await store.get(key);
52
+ if (existing) {
53
+ if (!(maxAgeMs !== void 0 && Date.now() - existing.updatedAt > maxAgeMs)) {
54
+ const resumed = await config.provider.resume({
55
+ id: existing.providerSandboxId,
56
+ signal: ctx.signal
57
+ });
58
+ if (resumed) {
59
+ await store.upsert({
60
+ ...existing,
61
+ latestRunId: ctx.runId,
62
+ updatedAt: Date.now()
63
+ });
64
+ return resumed;
65
+ }
66
+ if (existing.latestSnapshotId && caps.snapshots && config.provider.restoreSnapshot) {
67
+ const restored = await config.provider.restoreSnapshot({
68
+ snapshotId: existing.latestSnapshotId,
69
+ workspace: config.workspace,
70
+ policy: config.policy,
71
+ env: config.workspace?.secrets !== void 0 ? resolveAllSecrets(config.workspace.secrets) : void 0,
72
+ signal: ctx.signal
73
+ });
74
+ await store.upsert({
75
+ ...existing,
76
+ providerSandboxId: restored.id,
77
+ latestRunId: ctx.runId,
78
+ updatedAt: Date.now()
79
+ });
80
+ return restored;
81
+ }
82
+ }
83
+ }
84
+ const created = await config.provider.create({
85
+ id: key,
86
+ workspace: config.workspace,
87
+ policy: config.policy,
88
+ env: config.workspace?.secrets !== void 0 ? resolveAllSecrets(config.workspace.secrets) : void 0,
89
+ signal: ctx.signal
90
+ });
91
+ if (config.workspace) try {
92
+ await bootstrapWorkspace(created, config.workspace, { signal: ctx.signal });
93
+ } catch (error) {
94
+ await created.destroy().catch(() => {});
95
+ throw error;
96
+ }
97
+ let latestSnapshotId;
98
+ if (effectiveSnapshot === "after-setup" && caps.snapshots && created.snapshot) latestSnapshotId = (await created.snapshot("after-setup")).id;
99
+ await store.upsert({
100
+ key,
101
+ provider: config.provider.name,
102
+ providerSandboxId: created.id,
103
+ latestSnapshotId,
104
+ threadId: ctx.threadId,
105
+ latestRunId: ctx.runId,
106
+ updatedAt: Date.now()
107
+ });
108
+ return created;
109
+ });
110
+ };
111
+ const destroy = async (ctx) => {
112
+ const store = ctx.store ?? fallbackStore;
113
+ const key = computeSandboxKey(keyInputFor(ctx));
114
+ const existing = await store.get(key);
115
+ if (!existing) return;
116
+ const teardown = new AbortController();
117
+ const timer = setTimeout(() => teardown.abort(), DESTROY_TIMEOUT_MS);
118
+ try {
119
+ await config.provider.destroy({
120
+ id: existing.providerSandboxId,
121
+ signal: teardown.signal
122
+ });
123
+ } finally {
124
+ clearTimeout(timer);
125
+ }
126
+ await store.delete(key);
127
+ };
128
+ return {
129
+ id: config.id,
130
+ provider: config.provider,
131
+ workspace: config.workspace,
132
+ policy: config.policy,
133
+ lifecycle: config.lifecycle,
134
+ hooks: config.hooks,
135
+ fileEvents: config.fileEvents,
136
+ key: (ctx) => computeSandboxKey(keyInputFor(ctx)),
137
+ ensure,
138
+ destroy
139
+ };
124
140
  }
125
- export {
126
- defineSandbox
127
- };
128
- //# sourceMappingURL=sandbox.js.map
141
+ //#endregion
142
+ export { defineSandbox };
143
+
144
+ //# sourceMappingURL=sandbox.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.js","sources":["../../src/sandbox.ts"],"sourcesContent":["/**\n * `defineSandbox()` returns a LAZY controller — it never creates a sandbox at\n * definition time. `withSandbox()` (and advanced users) call `ensure()` to\n * resume-or-create, following: provider.resume → provider.restoreSnapshot →\n * create + bootstrap. The controller folds provider/workspace/policy/lifecycle\n * into a stable instance key and coordinates through the (optional) lock +\n * sandbox stores.\n */\nimport { bootstrapWorkspace } from './bootstrap'\nimport { resolveAllSecrets } from './secrets'\nimport { computeSandboxKey } from './key'\nimport { InMemoryLockStore, InMemorySandboxStore } from './store'\nimport type { SandboxFileHookEvent } from '@tanstack/ai'\nimport type { SandboxHandle, SandboxProvider } from './contracts'\nimport type { SandboxKeyInput } from './key'\nimport type { LockStore, SandboxStore } from './store'\nimport type { SandboxPolicy } from './policy'\nimport type { WorkspaceDefinition } from './workspace'\n\n/**\n * Sandbox-scoped hooks declared on `defineSandbox`. File hooks fire for every\n * create/change/delete during a chat run; lifecycle hooks fire server-side.\n */\nexport interface SandboxHooks {\n onFile?: (e: SandboxFileHookEvent) => void | Promise<void>\n onFileCreate?: (e: SandboxFileHookEvent) => void | Promise<void>\n onFileChange?: (e: SandboxFileHookEvent) => void | Promise<void>\n onFileDelete?: (e: SandboxFileHookEvent) => void | Promise<void>\n onReady?: (handle: SandboxHandle) => void | Promise<void>\n onError?: (err: unknown) => void | Promise<void>\n onDestroy?: () => void | Promise<void>\n}\n\nexport type ReuseStrategy = 'thread' | 'none'\nexport type SnapshotStrategy = 'after-setup' | 'after-run' | 'none'\n\nexport interface SandboxLifecycle {\n /** `'thread'` resumes one sandbox per thread; `'none'` is fresh per run. */\n reuse?: ReuseStrategy\n /** When to snapshot (provider-permitting). */\n snapshot?: SnapshotStrategy\n /** Hint for how long a provider should keep the sandbox warm between runs. */\n keepAlive?: string\n /** Destroy the sandbox after the run completes. */\n destroyOnComplete?: boolean\n /**\n * Maximum age of a sandbox record before it is discarded and re-created\n * instead of resumed. Accepts `'<n>h'` (hours) or `'<n>m'` (minutes),\n * e.g. `'2h'` or `'30m'`.\n */\n snapshotMaxAge?: string\n}\n\nexport interface SandboxConfig {\n id: string\n provider: SandboxProvider\n workspace?: WorkspaceDefinition\n policy?: SandboxPolicy\n lifecycle?: SandboxLifecycle\n /** Sandbox-scoped file/lifecycle hooks. */\n hooks?: SandboxHooks\n /** Watch the workspace for file events (default true). `false` disables the\n * watcher; `{ diff: true }` also emits a per-file `sandbox.file.diff` event. */\n fileEvents?: boolean | { diff?: boolean }\n}\n\n/** Context passed to `ensure()` by `withSandbox` (or advanced callers). */\nexport interface SandboxEnsureContext {\n threadId: string\n runId: string\n /** Persistence seam; falls back to an in-memory store when absent. */\n store?: SandboxStore\n /** Lock seam; falls back to an in-memory lock when absent. */\n locks?: LockStore\n tenant?: { userId?: string; orgId?: string }\n signal?: AbortSignal\n}\n\nexport interface SandboxDefinition {\n readonly id: string\n readonly provider: SandboxProvider\n readonly workspace?: WorkspaceDefinition\n readonly policy?: SandboxPolicy\n readonly lifecycle?: SandboxLifecycle\n /** Sandbox-scoped file/lifecycle hooks. */\n readonly hooks?: SandboxHooks\n /** Watch the workspace for file events (default true). `false` disables the\n * watcher; `{ diff: true }` also emits a per-file `sandbox.file.diff` event. */\n readonly fileEvents?: boolean | { diff?: boolean }\n /** Compound instance key for a given run context. */\n key: (ctx: SandboxEnsureContext) => string\n /** Resume-or-create the sandbox for this thread/run. */\n ensure: (ctx: SandboxEnsureContext) => Promise<SandboxHandle>\n /** Tear down the sandbox recorded for this key. */\n destroy: (ctx: SandboxEnsureContext) => Promise<void>\n}\n\n/**\n * Parse a human-readable duration string into milliseconds.\n * Supports `'<n>h'` (hours) and `'<n>m'` (minutes).\n * Returns `undefined` when the input is undefined or the format is unrecognised.\n */\nfunction parseMaxAgeMs(value: string | undefined): number | undefined {\n if (value === undefined) return undefined\n const hourMatch = /^(\\d+)h$/.exec(value)\n if (hourMatch) return Number(hourMatch[1]) * 60 * 60 * 1000\n const minuteMatch = /^(\\d+)m$/.exec(value)\n if (minuteMatch) return Number(minuteMatch[1]) * 60 * 1000\n return undefined\n}\n\n// Process-lifetime fallbacks shared across all definitions so concurrent\n// ensures for the same key serialize even without an injected store/lock.\nconst fallbackStore = new InMemorySandboxStore()\nconst fallbackLocks = new InMemoryLockStore()\n\nexport function defineSandbox(config: SandboxConfig): SandboxDefinition {\n const keyInputFor = (ctx: SandboxEnsureContext): SandboxKeyInput => ({\n threadId:\n config.lifecycle?.reuse === 'none'\n ? `${ctx.threadId}:${ctx.runId}`\n : ctx.threadId,\n sandboxId: config.id,\n providerName: config.provider.name,\n workspace: config.workspace,\n tenant: ctx.tenant,\n })\n\n const ensure = async (ctx: SandboxEnsureContext): Promise<SandboxHandle> => {\n const store = ctx.store ?? fallbackStore\n const locks = ctx.locks ?? fallbackLocks\n const key = computeSandboxKey(keyInputFor(ctx))\n const caps = config.provider.capabilities()\n\n return locks.withLock(`sandbox:${key}`, async () => {\n const effectiveSnapshot: SnapshotStrategy =\n config.lifecycle?.snapshot ?? (caps.snapshots ? 'after-setup' : 'none')\n const maxAgeMs = parseMaxAgeMs(config.lifecycle?.snapshotMaxAge)\n\n const existing = await store.get(key)\n if (existing) {\n // Check whether the record has exceeded snapshotMaxAge; if so,\n // discard and fall through to a fresh create.\n const tooOld =\n maxAgeMs !== undefined && Date.now() - existing.updatedAt > maxAgeMs\n\n if (!tooOld) {\n // 1) Try to reconnect to the still-running sandbox.\n const resumed = await config.provider.resume({\n id: existing.providerSandboxId,\n signal: ctx.signal,\n })\n if (resumed) {\n await store.upsert({\n ...existing,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return resumed\n }\n // 2) Else restore from the latest snapshot, if supported.\n if (\n existing.latestSnapshotId &&\n caps.snapshots &&\n config.provider.restoreSnapshot\n ) {\n const restored = await config.provider.restoreSnapshot({\n snapshotId: existing.latestSnapshotId,\n workspace: config.workspace,\n policy: config.policy,\n env:\n config.workspace?.secrets !== undefined\n ? resolveAllSecrets(config.workspace.secrets)\n : undefined,\n signal: ctx.signal,\n })\n await store.upsert({\n ...existing,\n providerSandboxId: restored.id,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return restored\n }\n }\n // 3) Else fall through and re-create under the same identity\n // (capability-aware degradation for ephemeral-disk providers, or\n // snapshotMaxAge TTL exceeded).\n }\n\n const created = await config.provider.create({\n // Deterministic id so consumers can reconstruct the provider sandbox\n // address from run context (not just from the store record).\n id: key,\n workspace: config.workspace,\n policy: config.policy,\n env:\n config.workspace?.secrets !== undefined\n ? resolveAllSecrets(config.workspace.secrets)\n : undefined,\n signal: ctx.signal,\n })\n\n if (config.workspace) {\n try {\n await bootstrapWorkspace(created, config.workspace, {\n signal: ctx.signal,\n })\n } catch (error) {\n // Bootstrap failed after the sandbox was created but before it was\n // recorded — destroy the orphan so a failed/retried run doesn't leak\n // a (billed) sandbox, then surface the original error.\n await created.destroy().catch(() => {})\n throw error\n }\n }\n\n let latestSnapshotId: string | undefined\n if (\n effectiveSnapshot === 'after-setup' &&\n caps.snapshots &&\n created.snapshot\n ) {\n latestSnapshotId = (await created.snapshot('after-setup')).id\n }\n\n await store.upsert({\n key,\n provider: config.provider.name,\n providerSandboxId: created.id,\n latestSnapshotId,\n threadId: ctx.threadId,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return created\n })\n }\n\n const destroy = async (ctx: SandboxEnsureContext): Promise<void> => {\n const store = ctx.store ?? fallbackStore\n const key = computeSandboxKey(keyInputFor(ctx))\n const existing = await store.get(key)\n if (!existing) return\n await config.provider.destroy({\n id: existing.providerSandboxId,\n signal: ctx.signal,\n })\n await store.delete(key)\n }\n\n return {\n id: config.id,\n provider: config.provider,\n workspace: config.workspace,\n policy: config.policy,\n lifecycle: config.lifecycle,\n hooks: config.hooks,\n fileEvents: config.fileEvents,\n key: (ctx) => computeSandboxKey(keyInputFor(ctx)),\n ensure,\n destroy,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sandbox.js","names":[],"sources":["../../src/sandbox.ts"],"sourcesContent":["/**\n * `defineSandbox()` returns a LAZY controller — it never creates a sandbox at\n * definition time. `withSandbox()` (and advanced users) call `ensure()` to\n * resume-or-create, following: provider.resume → provider.restoreSnapshot →\n * create + bootstrap. The controller folds provider/workspace/policy/lifecycle\n * into a stable instance key and coordinates through the (optional) lock +\n * sandbox stores.\n */\nimport { bootstrapWorkspace } from './bootstrap'\nimport { resolveAllSecrets } from './secrets'\nimport { computeSandboxKey } from './key'\nimport { InMemoryLockStore } from '@tanstack/ai/locks'\nimport type { LockStore } from '@tanstack/ai/locks'\nimport type { SandboxFileHookEvent } from '@tanstack/ai'\nimport { InMemorySandboxInstanceStore } from './instance-store'\nimport type { SandboxInstanceStore } from './instance-store'\nimport type { SandboxHandle, SandboxProvider } from './contracts'\nimport type { SandboxKeyInput } from './key'\nimport type { SandboxPolicy } from './policy'\nimport type { WorkspaceDefinition } from './workspace'\n\n/**\n * Sandbox-scoped hooks declared on `defineSandbox`. File hooks fire for every\n * create/change/delete during a chat run; lifecycle hooks fire server-side.\n */\nexport interface SandboxHooks {\n onFile?: (e: SandboxFileHookEvent) => void | Promise<void>\n onFileCreate?: (e: SandboxFileHookEvent) => void | Promise<void>\n onFileChange?: (e: SandboxFileHookEvent) => void | Promise<void>\n onFileDelete?: (e: SandboxFileHookEvent) => void | Promise<void>\n onReady?: (handle: SandboxHandle) => void | Promise<void>\n onError?: (err: unknown) => void | Promise<void>\n onDestroy?: () => void | Promise<void>\n}\n\nexport type ReuseStrategy = 'thread' | 'none'\nexport type SnapshotStrategy = 'after-setup' | 'after-run' | 'none'\n\nexport interface SandboxLifecycle {\n /** `'thread'` resumes one sandbox per thread; `'none'` is fresh per run. */\n reuse?: ReuseStrategy\n /** When to snapshot (provider-permitting). */\n snapshot?: SnapshotStrategy\n /** Hint for how long a provider should keep the sandbox warm between runs. */\n keepAlive?: string\n /** Destroy the sandbox after the run completes. */\n destroyOnComplete?: boolean\n /**\n * Maximum age of a sandbox record before it is discarded and re-created\n * instead of resumed. Accepts `'<n>h'` (hours) or `'<n>m'` (minutes),\n * e.g. `'2h'` or `'30m'`.\n */\n snapshotMaxAge?: string\n}\n\nexport interface SandboxConfig {\n id: string\n provider: SandboxProvider\n workspace?: WorkspaceDefinition\n policy?: SandboxPolicy\n lifecycle?: SandboxLifecycle\n /** Sandbox-scoped file/lifecycle hooks. */\n hooks?: SandboxHooks\n /** Watch the workspace for file events (default true). `false` disables the\n * watcher; `{ diff: true }` also emits a per-file `sandbox.file.diff` event. */\n fileEvents?: boolean | { diff?: boolean }\n}\n\n/** Context passed to `ensure()` by `withSandbox` (or advanced callers). */\nexport interface SandboxEnsureContext {\n threadId: string\n runId: string\n /** Persistence seam; falls back to an in-memory store when absent. */\n store?: SandboxInstanceStore\n /** Lock seam; falls back to an in-memory lock when absent. */\n locks?: LockStore\n tenant?: { userId?: string; orgId?: string }\n signal?: AbortSignal\n}\n\nexport interface SandboxDefinition {\n readonly id: string\n readonly provider: SandboxProvider\n readonly workspace?: WorkspaceDefinition\n readonly policy?: SandboxPolicy\n readonly lifecycle?: SandboxLifecycle\n /** Sandbox-scoped file/lifecycle hooks. */\n readonly hooks?: SandboxHooks\n /** Watch the workspace for file events (default true). `false` disables the\n * watcher; `{ diff: true }` also emits a per-file `sandbox.file.diff` event. */\n readonly fileEvents?: boolean | { diff?: boolean }\n /** Compound instance key for a given run context. */\n key: (ctx: SandboxEnsureContext) => string\n /** Resume-or-create the sandbox for this thread/run. */\n ensure: (ctx: SandboxEnsureContext) => Promise<SandboxHandle>\n /** Tear down the sandbox recorded for this key. */\n destroy: (ctx: SandboxEnsureContext) => Promise<void>\n}\n\n/**\n * Parse a human-readable duration string into milliseconds.\n * Supports `'<n>h'` (hours) and `'<n>m'` (minutes).\n * Returns `undefined` when the input is undefined or the format is unrecognised.\n */\nfunction parseMaxAgeMs(value: string | undefined): number | undefined {\n if (value === undefined) return undefined\n const hourMatch = /^(\\d+)h$/.exec(value)\n if (hourMatch) return Number(hourMatch[1]) * 60 * 60 * 1000\n const minuteMatch = /^(\\d+)m$/.exec(value)\n if (minuteMatch) return Number(minuteMatch[1]) * 60 * 1000\n return undefined\n}\n\n/**\n * Bound for the unfenced teardown `destroy` call (see `destroy` below). Long\n * enough that a slow provider API still completes, short enough that a wedged\n * one cannot pin the process forever.\n */\nconst DESTROY_TIMEOUT_MS = 60 * 1000\n\n// Process-lifetime fallbacks shared across all definitions so concurrent\n// ensures for the same key serialize even without an injected store/lock.\nconst fallbackStore = new InMemorySandboxInstanceStore()\nconst fallbackLocks = new InMemoryLockStore()\n\nexport function defineSandbox(config: SandboxConfig): SandboxDefinition {\n const keyInputFor = (ctx: SandboxEnsureContext): SandboxKeyInput => ({\n threadId:\n config.lifecycle?.reuse === 'none'\n ? `${ctx.threadId}:${ctx.runId}`\n : ctx.threadId,\n sandboxId: config.id,\n providerName: config.provider.name,\n workspace: config.workspace,\n tenant: ctx.tenant,\n })\n\n const ensure = async (ctx: SandboxEnsureContext): Promise<SandboxHandle> => {\n const store = ctx.store ?? fallbackStore\n const locks = ctx.locks ?? fallbackLocks\n const key = computeSandboxKey(keyInputFor(ctx))\n const caps = config.provider.capabilities()\n\n return locks.withLock(`sandbox:${key}`, async () => {\n const effectiveSnapshot: SnapshotStrategy =\n config.lifecycle?.snapshot ?? (caps.snapshots ? 'after-setup' : 'none')\n const maxAgeMs = parseMaxAgeMs(config.lifecycle?.snapshotMaxAge)\n\n const existing = await store.get(key)\n if (existing) {\n // Check whether the record has exceeded snapshotMaxAge; if so,\n // discard and fall through to a fresh create.\n const tooOld =\n maxAgeMs !== undefined && Date.now() - existing.updatedAt > maxAgeMs\n\n if (!tooOld) {\n // 1) Try to reconnect to the still-running sandbox.\n const resumed = await config.provider.resume({\n id: existing.providerSandboxId,\n signal: ctx.signal,\n })\n if (resumed) {\n await store.upsert({\n ...existing,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return resumed\n }\n // 2) Else restore from the latest snapshot, if supported.\n if (\n existing.latestSnapshotId &&\n caps.snapshots &&\n config.provider.restoreSnapshot\n ) {\n const restored = await config.provider.restoreSnapshot({\n snapshotId: existing.latestSnapshotId,\n workspace: config.workspace,\n policy: config.policy,\n env:\n config.workspace?.secrets !== undefined\n ? resolveAllSecrets(config.workspace.secrets)\n : undefined,\n signal: ctx.signal,\n })\n await store.upsert({\n ...existing,\n providerSandboxId: restored.id,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return restored\n }\n }\n // 3) Else fall through and re-create under the same identity\n // (capability-aware degradation for ephemeral-disk providers, or\n // snapshotMaxAge TTL exceeded).\n }\n\n const created = await config.provider.create({\n // Deterministic id so consumers can reconstruct the provider sandbox\n // address from run context (not just from the store record).\n id: key,\n workspace: config.workspace,\n policy: config.policy,\n env:\n config.workspace?.secrets !== undefined\n ? resolveAllSecrets(config.workspace.secrets)\n : undefined,\n signal: ctx.signal,\n })\n\n if (config.workspace) {\n try {\n await bootstrapWorkspace(created, config.workspace, {\n signal: ctx.signal,\n })\n } catch (error) {\n // Bootstrap failed after the sandbox was created but before it was\n // recorded — destroy the orphan so a failed/retried run doesn't leak\n // a (billed) sandbox, then surface the original error.\n await created.destroy().catch(() => {})\n throw error\n }\n }\n\n let latestSnapshotId: string | undefined\n if (\n effectiveSnapshot === 'after-setup' &&\n caps.snapshots &&\n created.snapshot\n ) {\n latestSnapshotId = (await created.snapshot('after-setup')).id\n }\n\n await store.upsert({\n key,\n provider: config.provider.name,\n providerSandboxId: created.id,\n latestSnapshotId,\n threadId: ctx.threadId,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return created\n })\n }\n\n const destroy = async (ctx: SandboxEnsureContext): Promise<void> => {\n const store = ctx.store ?? fallbackStore\n const key = computeSandboxKey(keyInputFor(ctx))\n const existing = await store.get(key)\n if (!existing) return\n /*\n * TEARDOWN IS DELIBERATELY NOT FENCED BY `ctx.signal`.\n *\n * `destroy` runs on every teardown path INCLUDING the one caused by that\n * very signal aborting, so forwarding it hands the provider a signal that is\n * already aborted: a provider that honors it does nothing and returns\n * successfully, and `store.delete` below then removes the only pointer to a\n * live, billed sandbox. `SandboxInstanceStore` has no `list` (see the note\n * at the top of `reclaim.ts`), so that sandbox is unreachable from then on.\n *\n * Same reasoning as `close()` never being fenced by the run claim (see\n * `fenceDurability` in `claim.ts`): cleanup must outlive whatever cancelled\n * the work. A fresh controller with its own bounded timeout keeps the call\n * from hanging forever without letting the caller's abort cancel it.\n */\n const teardown = new AbortController()\n const timer = setTimeout(() => teardown.abort(), DESTROY_TIMEOUT_MS)\n try {\n await config.provider.destroy({\n id: existing.providerSandboxId,\n signal: teardown.signal,\n })\n } finally {\n clearTimeout(timer)\n }\n await store.delete(key)\n }\n\n return {\n id: config.id,\n provider: config.provider,\n workspace: config.workspace,\n policy: config.policy,\n lifecycle: config.lifecycle,\n hooks: config.hooks,\n fileEvents: config.fileEvents,\n key: (ctx) => computeSandboxKey(keyInputFor(ctx)),\n ensure,\n destroy,\n }\n}\n"],"mappings":"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"}
@@ -1,59 +1,51 @@
1
- const REGISTRY = /* @__PURE__ */ Symbol("secrets.registry");
1
+ //#region src/secrets.ts
2
+ /** Internal symbol used to store the value registry on a Secrets object. */
3
+ var REGISTRY = Symbol("secrets.registry");
4
+ /** Create a typed secrets object from a plain record of name→value pairs. */
2
5
  function createSecrets(values) {
3
- const registry = new Map(Object.entries(values));
4
- const obj = {};
5
- for (const name of Object.keys(values)) {
6
- obj[name] = Object.freeze({ __secretName: name });
7
- }
8
- Object.defineProperty(obj, REGISTRY, {
9
- value: registry,
10
- enumerable: false,
11
- configurable: false,
12
- writable: false
13
- });
14
- return obj;
6
+ const registry = new Map(Object.entries(values));
7
+ const obj = {};
8
+ for (const name of Object.keys(values)) obj[name] = Object.freeze({ __secretName: name });
9
+ Object.defineProperty(obj, REGISTRY, {
10
+ value: registry,
11
+ enumerable: false,
12
+ configurable: false,
13
+ writable: false
14
+ });
15
+ return obj;
15
16
  }
17
+ /** Create a bearer-token marker that resolves to `Bearer <value>` at runtime. */
16
18
  function bearer(ref) {
17
- return Object.freeze({ __bearerRef: ref });
19
+ return Object.freeze({ __bearerRef: ref });
18
20
  }
21
+ /** Return true when `x` is a SecretRef. */
19
22
  function isSecretRef(x) {
20
- return typeof x === "object" && x !== null && typeof x["__secretName"] === "string";
23
+ return typeof x === "object" && x !== null && typeof x["__secretName"] === "string";
21
24
  }
25
+ /** Resolve a SecretRef to its plaintext value using the secrets object. */
22
26
  function resolveSecret(secrets, ref) {
23
- const registry = Reflect.get(secrets, REGISTRY);
24
- if (registry === void 0) {
25
- throw new Error(
26
- "resolveSecret: secrets object was not created by createSecrets"
27
- );
28
- }
29
- const value = registry.get(ref.__secretName);
30
- if (value === void 0) {
31
- throw new Error(`resolveSecret: unknown secret "${ref.__secretName}"`);
32
- }
33
- return value;
27
+ const registry = Reflect.get(secrets, REGISTRY);
28
+ if (registry === void 0) throw new Error("resolveSecret: secrets object was not created by createSecrets");
29
+ const value = registry.get(ref.__secretName);
30
+ if (value === void 0) throw new Error(`resolveSecret: unknown secret "${ref.__secretName}"`);
31
+ return value;
34
32
  }
33
+ /** Resolve a BearerRef to a `Bearer <value>` string. */
35
34
  function resolveBearer(secrets, ref) {
36
- return `Bearer ${resolveSecret(secrets, ref.__bearerRef)}`;
35
+ return `Bearer ${resolveSecret(secrets, ref.__bearerRef)}`;
37
36
  }
37
+ /**
38
+ * Resolve all secrets in a Secrets object to a plain `Record<string, string>`
39
+ * suitable for injecting into a process environment.
40
+ */
38
41
  function resolveAllSecrets(secrets) {
39
- const registry = Reflect.get(secrets, REGISTRY);
40
- if (registry === void 0) {
41
- throw new Error(
42
- "resolveAllSecrets: secrets object was not created by createSecrets"
43
- );
44
- }
45
- const result = {};
46
- for (const [key, value] of registry.entries()) {
47
- result[key] = value;
48
- }
49
- return result;
42
+ const registry = Reflect.get(secrets, REGISTRY);
43
+ if (registry === void 0) throw new Error("resolveAllSecrets: secrets object was not created by createSecrets");
44
+ const result = {};
45
+ for (const [key, value] of registry.entries()) result[key] = value;
46
+ return result;
50
47
  }
51
- export {
52
- bearer,
53
- createSecrets,
54
- isSecretRef,
55
- resolveAllSecrets,
56
- resolveBearer,
57
- resolveSecret
58
- };
59
- //# sourceMappingURL=secrets.js.map
48
+ //#endregion
49
+ export { bearer, createSecrets, isSecretRef, resolveAllSecrets, resolveBearer, resolveSecret };
50
+
51
+ //# sourceMappingURL=secrets.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"secrets.js","sources":["../../src/secrets.ts"],"sourcesContent":["/**\n * Type-safe secret references for sandbox workspace definitions.\n *\n * Values are stored in a Map under a non-enumerable symbol key on the returned\n * object so that `Object.keys(secrets)` only yields the ref names, never the\n * registry or the underlying plaintext values.\n */\n\n/** A reference to a named secret — carries only the name, never the value. */\nexport type SecretRef = { readonly __secretName: string }\n\n/**\n * A map of named SecretRef properties. The underlying value registry is stored\n * under a non-enumerable symbol so iterating the object never exposes it.\n */\nexport type Secrets<TKeys extends string = string> = {\n readonly [P in TKeys]: SecretRef\n}\n\n/** Internal symbol used to store the value registry on a Secrets object. */\nconst REGISTRY = Symbol('secrets.registry')\n\n/** Create a typed secrets object from a plain record of name→value pairs. */\nexport function createSecrets<T extends Record<string, string>>(\n values: T,\n): Secrets<keyof T & string> {\n const registry = new Map<string, string>(Object.entries(values))\n const obj = {} as Record<string, SecretRef>\n\n for (const name of Object.keys(values)) {\n obj[name] = Object.freeze({ __secretName: name })\n }\n\n Object.defineProperty(obj, REGISTRY, {\n value: registry,\n enumerable: false,\n configurable: false,\n writable: false,\n })\n\n return obj as Secrets<keyof T & string>\n}\n\n/** Marker type for a bearer-token value derived from a SecretRef. */\nexport type BearerRef = { readonly __bearerRef: SecretRef }\n\n/** Create a bearer-token marker that resolves to `Bearer <value>` at runtime. */\nexport function bearer(ref: SecretRef): BearerRef {\n return Object.freeze({ __bearerRef: ref })\n}\n\n/** Return true when `x` is a SecretRef. */\nexport function isSecretRef(x: unknown): x is SecretRef {\n return (\n typeof x === 'object' &&\n x !== null &&\n typeof (x as Record<string, unknown>)['__secretName'] === 'string'\n )\n}\n\n/** Resolve a SecretRef to its plaintext value using the secrets object. */\nexport function resolveSecret(secrets: Secrets, ref: SecretRef): string {\n const registry = Reflect.get(secrets, REGISTRY) as\n | Map<string, string>\n | undefined\n if (registry === undefined) {\n throw new Error(\n 'resolveSecret: secrets object was not created by createSecrets',\n )\n }\n const value = registry.get(ref.__secretName)\n if (value === undefined) {\n throw new Error(`resolveSecret: unknown secret \"${ref.__secretName}\"`)\n }\n return value\n}\n\n/** Resolve a BearerRef to a `Bearer <value>` string. */\nexport function resolveBearer(secrets: Secrets, ref: BearerRef): string {\n return `Bearer ${resolveSecret(secrets, ref.__bearerRef)}`\n}\n\n/**\n * Resolve all secrets in a Secrets object to a plain `Record<string, string>`\n * suitable for injecting into a process environment.\n */\nexport function resolveAllSecrets(secrets: Secrets): Record<string, string> {\n const registry = Reflect.get(secrets, REGISTRY) as\n | Map<string, string>\n | undefined\n if (registry === undefined) {\n throw new Error(\n 'resolveAllSecrets: secrets object was not created by createSecrets',\n )\n }\n const result: Record<string, string> = {}\n for (const [key, value] of registry.entries()) {\n result[key] = value\n }\n return result\n}\n"],"names":[],"mappings":"AAoBA,MAAM,kCAAkB,kBAAkB;AAGnC,SAAS,cACd,QAC2B;AAC3B,QAAM,WAAW,IAAI,IAAoB,OAAO,QAAQ,MAAM,CAAC;AAC/D,QAAM,MAAM,CAAA;AAEZ,aAAW,QAAQ,OAAO,KAAK,MAAM,GAAG;AACtC,QAAI,IAAI,IAAI,OAAO,OAAO,EAAE,cAAc,MAAM;AAAA,EAClD;AAEA,SAAO,eAAe,KAAK,UAAU;AAAA,IACnC,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,UAAU;AAAA,EAAA,CACX;AAED,SAAO;AACT;AAMO,SAAS,OAAO,KAA2B;AAChD,SAAO,OAAO,OAAO,EAAE,aAAa,KAAK;AAC3C;AAGO,SAAS,YAAY,GAA4B;AACtD,SACE,OAAO,MAAM,YACb,MAAM,QACN,OAAQ,EAA8B,cAAc,MAAM;AAE9D;AAGO,SAAS,cAAc,SAAkB,KAAwB;AACtE,QAAM,WAAW,QAAQ,IAAI,SAAS,QAAQ;AAG9C,MAAI,aAAa,QAAW;AAC1B,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,QAAM,QAAQ,SAAS,IAAI,IAAI,YAAY;AAC3C,MAAI,UAAU,QAAW;AACvB,UAAM,IAAI,MAAM,kCAAkC,IAAI,YAAY,GAAG;AAAA,EACvE;AACA,SAAO;AACT;AAGO,SAAS,cAAc,SAAkB,KAAwB;AACtE,SAAO,UAAU,cAAc,SAAS,IAAI,WAAW,CAAC;AAC1D;AAMO,SAAS,kBAAkB,SAA0C;AAC1E,QAAM,WAAW,QAAQ,IAAI,SAAS,QAAQ;AAG9C,MAAI,aAAa,QAAW;AAC1B,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,QAAM,SAAiC,CAAA;AACvC,aAAW,CAAC,KAAK,KAAK,KAAK,SAAS,WAAW;AAC7C,WAAO,GAAG,IAAI;AAAA,EAChB;AACA,SAAO;AACT;"}
1
+ {"version":3,"file":"secrets.js","names":[],"sources":["../../src/secrets.ts"],"sourcesContent":["/**\n * Type-safe secret references for sandbox workspace definitions.\n *\n * Values are stored in a Map under a non-enumerable symbol key on the returned\n * object so that `Object.keys(secrets)` only yields the ref names, never the\n * registry or the underlying plaintext values.\n */\n\n/** A reference to a named secret — carries only the name, never the value. */\nexport type SecretRef = { readonly __secretName: string }\n\n/**\n * A map of named SecretRef properties. The underlying value registry is stored\n * under a non-enumerable symbol so iterating the object never exposes it.\n */\nexport type Secrets<TKeys extends string = string> = {\n readonly [P in TKeys]: SecretRef\n}\n\n/** Internal symbol used to store the value registry on a Secrets object. */\nconst REGISTRY = Symbol('secrets.registry')\n\n/** Create a typed secrets object from a plain record of name→value pairs. */\nexport function createSecrets<T extends Record<string, string>>(\n values: T,\n): Secrets<keyof T & string> {\n const registry = new Map<string, string>(Object.entries(values))\n const obj = {} as Record<string, SecretRef>\n\n for (const name of Object.keys(values)) {\n obj[name] = Object.freeze({ __secretName: name })\n }\n\n Object.defineProperty(obj, REGISTRY, {\n value: registry,\n enumerable: false,\n configurable: false,\n writable: false,\n })\n\n return obj as Secrets<keyof T & string>\n}\n\n/** Marker type for a bearer-token value derived from a SecretRef. */\nexport type BearerRef = { readonly __bearerRef: SecretRef }\n\n/** Create a bearer-token marker that resolves to `Bearer <value>` at runtime. */\nexport function bearer(ref: SecretRef): BearerRef {\n return Object.freeze({ __bearerRef: ref })\n}\n\n/** Return true when `x` is a SecretRef. */\nexport function isSecretRef(x: unknown): x is SecretRef {\n return (\n typeof x === 'object' &&\n x !== null &&\n typeof (x as Record<string, unknown>)['__secretName'] === 'string'\n )\n}\n\n/** Resolve a SecretRef to its plaintext value using the secrets object. */\nexport function resolveSecret(secrets: Secrets, ref: SecretRef): string {\n const registry = Reflect.get(secrets, REGISTRY) as\n | Map<string, string>\n | undefined\n if (registry === undefined) {\n throw new Error(\n 'resolveSecret: secrets object was not created by createSecrets',\n )\n }\n const value = registry.get(ref.__secretName)\n if (value === undefined) {\n throw new Error(`resolveSecret: unknown secret \"${ref.__secretName}\"`)\n }\n return value\n}\n\n/** Resolve a BearerRef to a `Bearer <value>` string. */\nexport function resolveBearer(secrets: Secrets, ref: BearerRef): string {\n return `Bearer ${resolveSecret(secrets, ref.__bearerRef)}`\n}\n\n/**\n * Resolve all secrets in a Secrets object to a plain `Record<string, string>`\n * suitable for injecting into a process environment.\n */\nexport function resolveAllSecrets(secrets: Secrets): Record<string, string> {\n const registry = Reflect.get(secrets, REGISTRY) as\n | Map<string, string>\n | undefined\n if (registry === undefined) {\n throw new Error(\n 'resolveAllSecrets: secrets object was not created by createSecrets',\n )\n }\n const result: Record<string, string> = {}\n for (const [key, value] of registry.entries()) {\n result[key] = value\n }\n return result\n}\n"],"mappings":";;AAoBA,IAAM,WAAW,OAAO,kBAAkB;;AAG1C,SAAgB,cACd,QAC2B;CAC3B,MAAM,WAAW,IAAI,IAAoB,OAAO,QAAQ,MAAM,CAAC;CAC/D,MAAM,MAAM,CAAC;CAEb,KAAK,MAAM,QAAQ,OAAO,KAAK,MAAM,GACnC,IAAI,QAAQ,OAAO,OAAO,EAAE,cAAc,KAAK,CAAC;CAGlD,OAAO,eAAe,KAAK,UAAU;EACnC,OAAO;EACP,YAAY;EACZ,cAAc;EACd,UAAU;CACZ,CAAC;CAED,OAAO;AACT;;AAMA,SAAgB,OAAO,KAA2B;CAChD,OAAO,OAAO,OAAO,EAAE,aAAa,IAAI,CAAC;AAC3C;;AAGA,SAAgB,YAAY,GAA4B;CACtD,OACE,OAAO,MAAM,YACb,MAAM,QACN,OAAQ,EAA8B,oBAAoB;AAE9D;;AAGA,SAAgB,cAAc,SAAkB,KAAwB;CACtE,MAAM,WAAW,QAAQ,IAAI,SAAS,QAAQ;CAG9C,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,MACR,gEACF;CAEF,MAAM,QAAQ,SAAS,IAAI,IAAI,YAAY;CAC3C,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,kCAAkC,IAAI,aAAa,EAAE;CAEvE,OAAO;AACT;;AAGA,SAAgB,cAAc,SAAkB,KAAwB;CACtE,OAAO,UAAU,cAAc,SAAS,IAAI,WAAW;AACzD;;;;;AAMA,SAAgB,kBAAkB,SAA0C;CAC1E,MAAM,WAAW,QAAQ,IAAI,SAAS,QAAQ;CAG9C,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,MACR,oEACF;CAEF,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,CAAC,KAAK,UAAU,SAAS,QAAQ,GAC1C,OAAO,OAAO;CAEhB,OAAO;AACT"}
@@ -1,16 +1,24 @@
1
+ //#region src/setup-plan.ts
1
2
  function buildSetupPlan(input) {
2
- if (input === void 0) return [];
3
- if (Array.isArray(input)) {
4
- return input.map((command) => ({ kind: "serial", command }));
5
- }
6
- const groups = [];
7
- input({
8
- serial: (command) => groups.push({ kind: "serial", command }),
9
- parallel: (commands) => groups.push({ kind: "parallel", commands })
10
- });
11
- return groups;
3
+ if (input === void 0) return [];
4
+ if (Array.isArray(input)) return input.map((command) => ({
5
+ kind: "serial",
6
+ command
7
+ }));
8
+ const groups = [];
9
+ input({
10
+ serial: (command) => groups.push({
11
+ kind: "serial",
12
+ command
13
+ }),
14
+ parallel: (commands) => groups.push({
15
+ kind: "parallel",
16
+ commands
17
+ })
18
+ });
19
+ return groups;
12
20
  }
13
- export {
14
- buildSetupPlan
15
- };
16
- //# sourceMappingURL=setup-plan.js.map
21
+ //#endregion
22
+ export { buildSetupPlan };
23
+
24
+ //# sourceMappingURL=setup-plan.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup-plan.js","sources":["../../src/setup-plan.ts"],"sourcesContent":["export type SetupGroup =\n | { kind: 'serial'; command: string }\n | { kind: 'parallel'; commands: Array<string> }\n\nexport interface SetupBuilder {\n serial: (command: string) => void\n parallel: (commands: Array<string>) => void\n}\n\nexport type SetupInput = Array<string> | ((builder: SetupBuilder) => void)\n\nexport function buildSetupPlan(\n input: SetupInput | undefined,\n): Array<SetupGroup> {\n if (input === undefined) return []\n if (Array.isArray(input)) {\n return input.map((command) => ({ kind: 'serial', command }))\n }\n const groups: Array<SetupGroup> = []\n input({\n serial: (command) => groups.push({ kind: 'serial', command }),\n parallel: (commands) => groups.push({ kind: 'parallel', commands }),\n })\n return groups\n}\n"],"names":[],"mappings":"AAWO,SAAS,eACd,OACmB;AACnB,MAAI,UAAU,OAAW,QAAO,CAAA;AAChC,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,CAAC,aAAa,EAAE,MAAM,UAAU,UAAU;AAAA,EAC7D;AACA,QAAM,SAA4B,CAAA;AAClC,QAAM;AAAA,IACJ,QAAQ,CAAC,YAAY,OAAO,KAAK,EAAE,MAAM,UAAU,SAAS;AAAA,IAC5D,UAAU,CAAC,aAAa,OAAO,KAAK,EAAE,MAAM,YAAY,SAAA,CAAU;AAAA,EAAA,CACnE;AACD,SAAO;AACT;"}
1
+ {"version":3,"file":"setup-plan.js","names":[],"sources":["../../src/setup-plan.ts"],"sourcesContent":["export type SetupGroup =\n | { kind: 'serial'; command: string }\n | { kind: 'parallel'; commands: Array<string> }\n\nexport interface SetupBuilder {\n serial: (command: string) => void\n parallel: (commands: Array<string>) => void\n}\n\nexport type SetupInput = Array<string> | ((builder: SetupBuilder) => void)\n\nexport function buildSetupPlan(\n input: SetupInput | undefined,\n): Array<SetupGroup> {\n if (input === undefined) return []\n if (Array.isArray(input)) {\n return input.map((command) => ({ kind: 'serial', command }))\n }\n const groups: Array<SetupGroup> = []\n input({\n serial: (command) => groups.push({ kind: 'serial', command }),\n parallel: (commands) => groups.push({ kind: 'parallel', commands }),\n })\n return groups\n}\n"],"mappings":";AAWA,SAAgB,eACd,OACmB;CACnB,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC;CACjC,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,aAAa;EAAE,MAAM;EAAU;CAAQ,EAAE;CAE7D,MAAM,SAA4B,CAAC;CACnC,MAAM;EACJ,SAAS,YAAY,OAAO,KAAK;GAAE,MAAM;GAAU;EAAQ,CAAC;EAC5D,WAAW,aAAa,OAAO,KAAK;GAAE,MAAM;GAAY;EAAS,CAAC;CACpE,CAAC;CACD,OAAO;AACT"}
@@ -21,6 +21,14 @@ export interface BootstrapShell {
21
21
  export interface BootstrapShellOptions {
22
22
  /** Working directory to start the shell in (passed as ProcessOptions.cwd). */
23
23
  cwd?: string;
24
+ /**
25
+ * Belt-and-braces deadline for a single `run()` to see its sentinel. The
26
+ * primary termination condition is the stdout stream ending (see
27
+ * {@link createBootstrapShell}); this only catches a shell that is alive,
28
+ * silent, and never going to answer. Generous by default because setup steps
29
+ * legitimately run for a long time (`npm install`, image pulls).
30
+ */
31
+ commandTimeoutMs?: number;
24
32
  }
25
33
  /**
26
34
  * Spawn one `sh` process and return a {@link BootstrapShell} that drives it