@zocomputer/agent-sdk 0.5.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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +673 -0
  3. package/dist/attachments.js +52 -0
  4. package/dist/gateway-fetch.js +67 -0
  5. package/dist/index.js +4169 -0
  6. package/dist/initiator-auth.js +49 -0
  7. package/dist/platform/agent-sandbox/index.js +691 -0
  8. package/dist/platform/cloud-tools/image.js +498 -0
  9. package/dist/platform/cloud-tools/index.js +507 -0
  10. package/dist/platform/cloud-tools/web-search.js +87 -0
  11. package/dist/platform/runtime-ai/gateway.js +87 -0
  12. package/dist/platform/runtime-ai/index.js +91 -0
  13. package/dist/platform/runtime-ai/register.js +88 -0
  14. package/dist/platform/runtime-ai/session-fetch.js +54 -0
  15. package/dist/platform/runtime-auth/index.js +141 -0
  16. package/dist/state-files.js +287 -0
  17. package/dist/state-sandbox.js +383 -0
  18. package/dist/state.js +29 -0
  19. package/dist/steer-inbox.js +84 -0
  20. package/dist/steer.js +83 -0
  21. package/package.json +143 -0
  22. package/platform/agent-sandbox/api-client.ts +196 -0
  23. package/platform/agent-sandbox/index.ts +27 -0
  24. package/platform/agent-sandbox/pure.ts +83 -0
  25. package/platform/agent-sandbox/sftp.ts +141 -0
  26. package/platform/agent-sandbox/ssh-connection.ts +165 -0
  27. package/platform/agent-sandbox/ssh-exec.ts +98 -0
  28. package/platform/agent-sandbox/ssh-session.ts +487 -0
  29. package/platform/agent-sandbox/zo-backend.ts +88 -0
  30. package/platform/agent-sandbox/zo-sandbox.ts +39 -0
  31. package/platform/cloud-tools/image-path.ts +44 -0
  32. package/platform/cloud-tools/image.ts +225 -0
  33. package/platform/cloud-tools/index.ts +22 -0
  34. package/platform/cloud-tools/state-files.ts +368 -0
  35. package/platform/cloud-tools/tool-meta.ts +32 -0
  36. package/platform/cloud-tools/web-search.ts +15 -0
  37. package/platform/runtime-ai/gateway.ts +76 -0
  38. package/platform/runtime-ai/index.ts +20 -0
  39. package/platform/runtime-ai/register.ts +26 -0
  40. package/platform/runtime-ai/session-fetch.ts +124 -0
  41. package/platform/runtime-auth/index.ts +331 -0
  42. package/src/async-tasks.ts +273 -0
  43. package/src/attachments.ts +109 -0
  44. package/src/backgroundable.ts +88 -0
  45. package/src/bounded-output.ts +159 -0
  46. package/src/dir-conventions.ts +238 -0
  47. package/src/extract/cache.ts +40 -0
  48. package/src/extract/docx.ts +18 -0
  49. package/src/extract/pdf.ts +54 -0
  50. package/src/extract/sheet.ts +56 -0
  51. package/src/file-kind.ts +258 -0
  52. package/src/file-view.ts +80 -0
  53. package/src/gateway-fetch.ts +115 -0
  54. package/src/glob-match.ts +13 -0
  55. package/src/hooks.ts +213 -0
  56. package/src/index.ts +419 -0
  57. package/src/initiator-auth.ts +81 -0
  58. package/src/instructions.ts +224 -0
  59. package/src/list-files.ts +41 -0
  60. package/src/mock-model.ts +572 -0
  61. package/src/park-delivery.ts +247 -0
  62. package/src/path-locks.ts +52 -0
  63. package/src/read-file-content.ts +142 -0
  64. package/src/read-text.ts +40 -0
  65. package/src/redeliver.ts +155 -0
  66. package/src/run.ts +159 -0
  67. package/src/sandbox-io.ts +414 -0
  68. package/src/state-files.ts +460 -0
  69. package/src/state-sandbox.ts +710 -0
  70. package/src/state.ts +96 -0
  71. package/src/steer-inbox.ts +105 -0
  72. package/src/steer-tool.ts +83 -0
  73. package/src/steer.ts +146 -0
  74. package/src/task.ts +320 -0
  75. package/src/tools/bash.ts +143 -0
  76. package/src/tools/edit.ts +58 -0
  77. package/src/tools/glob.ts +56 -0
  78. package/src/tools/grep.ts +188 -0
  79. package/src/tools/read.ts +319 -0
  80. package/src/tools/tasks.ts +241 -0
  81. package/src/tools/webfetch.ts +368 -0
  82. package/src/tools/write.ts +42 -0
  83. package/src/walk.ts +112 -0
  84. package/src/watch-output.ts +104 -0
  85. package/src/web-fetch.ts +324 -0
  86. package/src/web-page.ts +179 -0
  87. package/src/workspace-io.ts +225 -0
  88. package/src/workspace.ts +41 -0
@@ -0,0 +1,165 @@
1
+ /** Scoped SSH credentials the control plane mints for one sandbox. */
2
+ export interface SshSandboxAccess {
3
+ readonly sandboxId: string;
4
+ readonly sshHost: string;
5
+ /** Username = the scoped access token (Daytona's SSH gateway model). */
6
+ readonly sshUser: string;
7
+ /** ISO timestamp when the token expires; we re-mint before then. */
8
+ readonly expiresAt: string;
9
+ }
10
+
11
+ // The SSH connection lifecycle as an explicit, testable state machine, lifted
12
+ // out of the session surface so its transitions live in one auditable place
13
+ // (the runtime's recurring bugs all came from ad-hoc interleavings here).
14
+ //
15
+ // States: idle (no connection) → connecting (a connect in flight) → connected
16
+ // (live) → back to idle on a socket drop, or disposed (terminal). The scoped
17
+ // SSH token (`heldAccess`) is retained across a socket drop so a pure reconnect
18
+ // reuses it instead of re-minting; it's dropped only when expired or on dispose.
19
+ //
20
+ // `connect` is injected so the manager is unit-tested without a live SSH socket;
21
+ // the runtime passes a real ssh2-backed connector.
22
+
23
+ /**
24
+ * Minimal live-connection handle the manager drives. Generic over the carried
25
+ * client `C` (the runtime carries an ssh2 `Client`, which `run` execs against;
26
+ * tests carry a stub) — the manager only ever uses `end`/`onClose`.
27
+ */
28
+ export interface ManagedConn<C = unknown> {
29
+ /** The underlying client (ssh2 `Client` in the runtime); used by callers, not the manager. */
30
+ readonly client: C;
31
+ /** Close the connection (idempotent). */
32
+ end(): void;
33
+ /** Register a one-shot callback for when the connection drops. */
34
+ onClose(cb: () => void): void;
35
+ }
36
+
37
+ /** Opens a connection for the given scoped access, or rejects. */
38
+ export type Connector<C> = (access: SshSandboxAccess) => Promise<ManagedConn<C>>;
39
+
40
+ /** Is the access token expired (or unparsable → treat as expired, fail-safe)? */
41
+ function isExpired(access: SshSandboxAccess, skewMs: number, now: number): boolean {
42
+ const expiry = Date.parse(access.expiresAt);
43
+ if (Number.isNaN(expiry)) return true;
44
+ return expiry - skewMs <= now;
45
+ }
46
+
47
+ export interface SshConnectionManagerOptions<C> {
48
+ /** Mint fresh scoped access (the runtime's control-plane call). */
49
+ readonly acquireAccess: () => Promise<SshSandboxAccess>;
50
+ /** Open a connection for an access (injectable; real impl wraps ssh2). */
51
+ readonly connect: Connector<C>;
52
+ /** Re-mint this many ms before the token's actual expiry. */
53
+ readonly expirySkewMs: number;
54
+ /** Clock, injectable for tests. Defaults to `Date.now`. */
55
+ readonly now?: () => number;
56
+ }
57
+
58
+ /**
59
+ * Owns one sandbox's SSH connection across its lifetime. `ensure()` returns a
60
+ * live connection, (re)connecting/(re)minting as the rules below require;
61
+ * `dispose()` shuts it down terminally.
62
+ */
63
+ export class SshConnectionManager<C> {
64
+ private conn: ManagedConn<C> | null = null;
65
+ private access: SshSandboxAccess | null = null;
66
+ private connecting: Promise<ManagedConn<C>> | null = null;
67
+ private disposed = false;
68
+ private sandboxId: string | null = null;
69
+ private readonly opts: SshConnectionManagerOptions<C>;
70
+ private readonly now: () => number;
71
+
72
+ constructor(opts: SshConnectionManagerOptions<C>) {
73
+ this.opts = opts;
74
+ this.now = opts.now ?? Date.now;
75
+ }
76
+
77
+ /** The provider sandbox id once a connection has been established, else null. */
78
+ currentSandboxId(): string | null {
79
+ return this.sandboxId;
80
+ }
81
+
82
+ private expired(access: SshSandboxAccess): boolean {
83
+ return isExpired(access, this.opts.expirySkewMs, this.now());
84
+ }
85
+
86
+ /**
87
+ * Get a live connection. Reuses a live, non-expired one; coalesces concurrent
88
+ * callers onto one in-flight connect; reconnects with the held (still-valid)
89
+ * token on a pure socket drop, and only re-mints when the token is expired/
90
+ * absent. A connect that fails on a reused token (gateway may have revoked it
91
+ * on a sandbox restart) re-mints once and retries.
92
+ */
93
+ async ensure(): Promise<ManagedConn<C>> {
94
+ if (this.disposed) throw new Error("zo sandbox: session is disposed");
95
+ if (this.conn !== null && this.access !== null && !this.expired(this.access)) {
96
+ return this.conn;
97
+ }
98
+ if (this.connecting !== null) return await this.connecting;
99
+
100
+ const held = this.access;
101
+ this.conn?.end();
102
+ this.conn = null;
103
+ this.connecting = this.openConnection(held);
104
+ try {
105
+ return await this.connecting;
106
+ } finally {
107
+ this.connecting = null;
108
+ }
109
+ }
110
+
111
+ private async openConnection(held: SshSandboxAccess | null): Promise<ManagedConn<C>> {
112
+ // dispose() can land at any await below; bail at each step so a disposed
113
+ // session never spends a control-plane mint or opens a doomed connection.
114
+ const abortIfDisposed = (): void => {
115
+ if (this.disposed) throw new Error("zo sandbox: session disposed during connect");
116
+ };
117
+
118
+ // Reuse the held token if still valid (socket just dropped); else mint fresh.
119
+ let used: SshSandboxAccess;
120
+ if (held !== null && !this.expired(held)) {
121
+ used = held;
122
+ } else {
123
+ abortIfDisposed(); // don't mint for a disposed session
124
+ used = await this.opts.acquireAccess();
125
+ }
126
+ abortIfDisposed(); // don't open a connection for a disposed session
127
+ let c: ManagedConn<C>;
128
+ try {
129
+ c = await this.opts.connect(used);
130
+ } catch (error) {
131
+ // A reused token may have been revoked server-side (sandbox restart) —
132
+ // re-mint once and retry. A fresh-token failure is a real error.
133
+ if (used === held) {
134
+ abortIfDisposed();
135
+ used = await this.opts.acquireAccess();
136
+ abortIfDisposed();
137
+ c = await this.opts.connect(used);
138
+ } else {
139
+ throw error;
140
+ }
141
+ }
142
+ // dispose() during the connect → this connection is orphaned; close it.
143
+ if (this.disposed) {
144
+ c.end();
145
+ throw new Error("zo sandbox: session disposed during connect");
146
+ }
147
+ // On a socket drop, fall back to idle — but KEEP the token so the next
148
+ // ensure() reconnects with it rather than re-minting needlessly.
149
+ c.onClose(() => {
150
+ if (this.conn === c) this.conn = null;
151
+ });
152
+ this.conn = c;
153
+ this.access = used;
154
+ this.sandboxId = used.sandboxId;
155
+ return c;
156
+ }
157
+
158
+ /** Terminal shutdown: close the connection and refuse further ensure() calls. */
159
+ dispose(): void {
160
+ this.disposed = true;
161
+ this.conn?.end();
162
+ this.conn = null;
163
+ this.access = null;
164
+ }
165
+ }
@@ -0,0 +1,98 @@
1
+ import type { ClientChannel } from "ssh2";
2
+
3
+ // The one place ssh2 exec-channel completion is reconciled, so run/spawn/sftp
4
+ // can't drift on it. Two concerns live here, both correct-by-construction:
5
+ // 1. exit reconciliation — prefer the `exit` event's (code, signal) over what
6
+ // `close` carried; a signal → SIGNAL_EXIT_CODE, never a clean 0.
7
+ // 2. abort precedence — if the abort signal is (or becomes) aborted, the
8
+ // command rejects with the abort reason, EVEN IF the channel also exited in
9
+ // the same tick. Abort structurally wins because the single settle() point
10
+ // checks `aborted` before it ever resolves success.
11
+
12
+ /** Exit code we report when a command is killed by a signal (no numeric code). */
13
+ export const SIGNAL_EXIT_CODE = 137; // 128 + SIGKILL(9), the conventional shell value
14
+
15
+ /** The reconciled outcome of an exec channel that exited (not aborted/errored). */
16
+ export interface ExecExit {
17
+ /** Reconciled exit code; a signal termination maps to SIGNAL_EXIT_CODE. */
18
+ readonly exitCode: number;
19
+ /** The terminating signal name, if the channel reported one (else null). */
20
+ readonly signal: string | null;
21
+ }
22
+
23
+ /** The abort reason as an Error (falls back to a generic one). */
24
+ function abortError(signal: AbortSignal): Error {
25
+ return signal.reason instanceof Error
26
+ ? signal.reason
27
+ : new Error(typeof signal.reason === "string" ? signal.reason : "aborted");
28
+ }
29
+
30
+ /**
31
+ * Await an exec channel's completion, with abort taking precedence over a
32
+ * same-tick exit.
33
+ *
34
+ * - Resolves `ExecExit` when the channel exits/closes (and not aborted).
35
+ * - Rejects with the abort reason if `abortSignal` is or becomes aborted — and
36
+ * closes the channel so the remote process is torn down. Wins a same-tick race
37
+ * with exit because `settle()` re-checks `aborted` before resolving success.
38
+ * - Rejects with the channel error on a stream `error`.
39
+ *
40
+ * Resolves only on exit/close/error/abort; attach `data`/`stderr` separately.
41
+ */
42
+ export function awaitCommand(
43
+ stream: ClientChannel,
44
+ abortSignal?: AbortSignal,
45
+ ): Promise<ExecExit> {
46
+ return new Promise<ExecExit>((resolve, reject) => {
47
+ let code: number | null = null;
48
+ let signal: string | null = null;
49
+ let sawExit = false;
50
+ let settled = false;
51
+
52
+ const onAbort = (): void => {
53
+ // close() tears down the remote process; settle() turns this into a reject.
54
+ try {
55
+ stream.close();
56
+ } catch {
57
+ // already closed
58
+ }
59
+ settle();
60
+ };
61
+
62
+ // The single settle point. Abort always wins: if the signal is aborted we
63
+ // reject with its reason regardless of whether the channel also exited.
64
+ const settle = (channelError?: Error): void => {
65
+ if (settled) return;
66
+ settled = true;
67
+ abortSignal?.removeEventListener("abort", onAbort);
68
+ if (abortSignal?.aborted) { reject(abortError(abortSignal)); return; }
69
+ if (channelError !== undefined) { reject(channelError); return; }
70
+ const sig = signal;
71
+ const reconciled = sig != null ? SIGNAL_EXIT_CODE : (code ?? 0);
72
+ resolve({ exitCode: reconciled, signal: sig });
73
+ };
74
+
75
+ // Already aborted before we wired up — close + reject without hanging.
76
+ if (abortSignal?.aborted) {
77
+ onAbort();
78
+ return;
79
+ }
80
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
81
+
82
+ stream
83
+ .on("exit", (c: number | null, s?: string | null) => {
84
+ sawExit = true;
85
+ code = c;
86
+ signal = s ?? null;
87
+ })
88
+ .on("close", (closeCode?: number | null, closeSignal?: string | null) => {
89
+ // Prefer the authoritative `exit` values; fall back to what close carried.
90
+ if (!sawExit) {
91
+ code = closeCode ?? null;
92
+ signal = closeSignal ?? null;
93
+ }
94
+ settle();
95
+ })
96
+ .on("error", (e: Error) => settle(e));
97
+ });
98
+ }