@workerdeck/server 2.14.0 → 3.0.1

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.
package/build/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, Server, ServerResponse } from "node:http";
2
- import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, CostLedgerState, EngineAdapter, EngineSessionOptions, HostToolDefinition, LanguageModel, LocalShellSource, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SHELL_REFUSAL, SessionInstructions, SessionRunnerConfig, ToolExecutionCall, ToolExecutionResult, ToolExecutor, ToolSet } from "@workerdeck/core";
2
+ import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, CostLedgerState, EngineAdapter, EngineSessionOptions, HostToolDefinition, LanguageModel, LocalShellSource, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SHELL_REFUSAL, SessionInstructions, SessionRunnerConfig, ShellDirectory, ToolExecutionCall, ToolExecutionResult, ToolExecutor, ToolSet } from "@workerdeck/core";
3
3
  import { JobQueue, QueueAdapter } from "@workerdeck/queue";
4
4
  import { ByModel, CreateSessionRequest, JobEvent, MessageAttachment, ModelRate, ProfileEngine, ProfileInfo, ProfileSpend, ProfileUsage, ProviderConfig, SdkSessionSummary, ServerFrame, SessionCapability, SessionInfo, SessionNotification, SessionWebhookConfig, ShellEndReason, ShellInfo, ShellOwner } from "@workerdeck/protocol";
5
5
  //#region src/services/bridge.d.ts
@@ -188,7 +188,7 @@ type ShellAttachment = {
188
188
  detach: () => void;
189
189
  };
190
190
  type ShellOutputQuery = {
191
- view: 'text' | 'raw';
191
+ view: 'text' | 'raw' | 'screen';
192
192
  tail?: number;
193
193
  };
194
194
  type ShellErrorContext = {
@@ -196,6 +196,9 @@ type ShellErrorContext = {
196
196
  sessionId?: string;
197
197
  shellId?: string;
198
198
  };
199
+ type ShellDirectoryDeps = {
200
+ runnerFor?: (sessionId: string) => Runner | undefined;
201
+ };
199
202
  type ShellRegistryOptions = {
200
203
  generation: string;
201
204
  artifactDir: string | null;
@@ -228,18 +231,22 @@ type ShellRegistry = {
228
231
  list: (sessionId: string) => ShellInfo[];
229
232
  running: () => ShellInfo[];
230
233
  kill: (sessionId: string, shellId: string, reason?: ShellEndReason) => ShellInfo | undefined;
234
+ setAgentWrite: (sessionId: string, shellId: string, enabled: boolean) => ShellInfo | undefined;
231
235
  killAll: (reason: ShellEndReason) => number;
232
236
  killAllSync: () => void;
233
237
  attach: (sessionId: string, shellId: string, sink: ShellSink) => Promise<ShellAttachment>;
234
238
  write: (sessionId: string, shellId: string, data: string) => void;
235
239
  resize: (sessionId: string, shellId: string, size: ShellSize) => ShellSize;
236
240
  output: (sessionId: string, shellId: string, query: ShellOutputQuery) => Promise<string | undefined>;
241
+ applicationCursorKeys: (sessionId: string, shellId: string) => Promise<boolean>;
242
+ changed: (sessionId: string, shellId: string, timeoutMs: number) => Promise<void>;
237
243
  hydrate: () => Promise<void>;
238
244
  sweep: () => Promise<void>;
239
245
  flush: () => Promise<void>;
240
246
  decorate: (info: SessionInfo) => SessionInfo;
241
247
  watch: (runner: Runner) => () => void;
242
248
  };
249
+ declare function createShellDirectory(registry: ShellRegistry, deps?: ShellDirectoryDeps): ShellDirectory;
243
250
  declare function shellPermitted(shells: ShellRegistry | null, runner: Runner, operator: boolean): boolean;
244
251
  declare function createShellRegistry(options: ShellRegistryOptions): ShellRegistry;
245
252
  //#endregion
@@ -271,6 +278,7 @@ declare class SpendLedger {
271
278
  declare function createFileSpendStore(path: string, onError?: (error: unknown) => void): SpendStore;
272
279
  //#endregion
273
280
  //#region src/options.d.ts
281
+ type ShellAgentWriteOption = 'read-only' | 'gated' | 'allow';
274
282
  type SdkSessionLister = (options: {
275
283
  dir?: string;
276
284
  limit?: number;
@@ -296,6 +304,7 @@ type WorkerServerOptions = {
296
304
  artifactMaxBytes?: number;
297
305
  artifactTtlMs?: number;
298
306
  maxRunningPerSession?: number;
307
+ agentWrite?: ShellAgentWriteOption;
299
308
  };
300
309
  attachments?: {
301
310
  maxFileBytes?: number;
@@ -502,5 +511,5 @@ declare class ProfileUsageTracker {
502
511
  usage(profile: string, now?: number): ProfileUsage | undefined;
503
512
  }
504
513
  //#endregion
505
- export { AttachmentStore, type AttachmentStoreOptions, type Authenticator, BridgeHub, type BridgeHubOptions, type CarriedSession, type EngineRunnerContext, type FileSessionStoreOptions, MemorySessionStore, type ParkedSessionRecord, type ProcessRow, type ProcessTable, type ProducedFile, ProducedFileStore, type ProfileStore, ProfileUsageTracker, type ProviderRunnerOptions, type QueueServerOptions, type ReloadPlan, SHELL_REFUSAL, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, type ShellRegistry, type ShellRegistryOptions, type ShellSink, SpendLedger, type SpendRecord, type SpendStore, type StoredSessionRecord, type StoredShellIndex, type StoredShellRecord, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellRegistry, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord };
514
+ export { AttachmentStore, type AttachmentStoreOptions, type Authenticator, BridgeHub, type BridgeHubOptions, type CarriedSession, type EngineRunnerContext, type FileSessionStoreOptions, MemorySessionStore, type ParkedSessionRecord, type ProcessRow, type ProcessTable, type ProducedFile, ProducedFileStore, type ProfileStore, ProfileUsageTracker, type ProviderRunnerOptions, type QueueServerOptions, type ReloadPlan, SHELL_REFUSAL, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, type ShellAgentWriteOption, type ShellDirectoryDeps, type ShellRegistry, type ShellRegistryOptions, type ShellSink, SpendLedger, type SpendRecord, type SpendStore, type StoredSessionRecord, type StoredShellIndex, type StoredShellRecord, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellDirectory, createShellRegistry, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord };
506
515
  //# sourceMappingURL=index.d.mts.map
package/build/index.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { WebSocketServer } from "ws";
4
- import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SessionRunner, attachmentKind, checkClaudeAuth, clampShellTail, createEngineSession, getEngineAdapter, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellSummary, shellTail, ttyText } from "@workerdeck/core";
4
+ import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SessionRunner, TTY_REDRAW_CARRY, agentMayWrite, attachmentKind, checkClaudeAuth, clampShellTail, createEngineSession, encodeShellKeys, getEngineAdapter, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellOwnershipRefusal, shellSummary, shellTail, shellToolNeedsCard, ttyRedraws, ttyText } from "@workerdeck/core";
5
5
  import { JobQueue } from "@workerdeck/queue";
6
- import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_ARTIFACT_MAX_BYTES, SHELL_ARTIFACT_TTL_MS, SHELL_ATTACH_REPLAY_BYTES, SHELL_COLS, SHELL_COMMAND_MAX, SHELL_INPUT_MAX, SHELL_LABEL_MAX, SHELL_LINGER_MS, SHELL_MAX_COLS, SHELL_MAX_ROWS, SHELL_MAX_RUNNING_PER_SESSION, SHELL_MAX_RUNNING_TOTAL, SHELL_MIN_COLS, SHELL_MIN_ROWS, SHELL_ROWS, SHELL_SPILL_BYTES, SHELL_TAIL_FLUSH_MS, SHELL_TAIL_RING_BYTES, costOfByModel, imagePartRef, isJobRun, mergeByModel, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, setPricingOverrides, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
6
+ import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_ARTIFACT_MAX_BYTES, SHELL_ARTIFACT_TTL_MS, SHELL_ATTACH_REPLAY_BYTES, SHELL_COLS, SHELL_COMMAND_MAX, SHELL_INPUT_MAX, SHELL_LABEL_MAX, SHELL_LINGER_MS, SHELL_MAX_COLS, SHELL_MAX_ROWS, SHELL_MAX_RUNNING_PER_SESSION, SHELL_MAX_RUNNING_TOTAL, SHELL_MIN_COLS, SHELL_MIN_ROWS, SHELL_READ_MAX_LINES, SHELL_ROWS, SHELL_SPILL_BYTES, SHELL_TAIL_FLUSH_MS, SHELL_TAIL_RING_BYTES, SHELL_WAIT_DEFAULT_MS, SHELL_WAIT_MAX_MS, costOfByModel, imagePartRef, isJobRun, mergeByModel, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, setPricingOverrides, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
7
7
  import { arch, homedir, hostname, platform, tmpdir } from "node:os";
8
8
  import { closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
9
9
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
10
  import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
11
+ import xtermHeadless from "@xterm/headless";
11
12
  import { spawnSync } from "node:child_process";
12
13
  //#region src/lib/http.ts
13
14
  const CONTENT_TYPES = {
@@ -197,7 +198,7 @@ function parseSessionRoute(basePath, url) {
197
198
  };
198
199
  if (parts[1] === "shells" && parts.length <= 4) {
199
200
  const action = parts[3];
200
- if (action !== void 0 && action !== "output" && action !== "kill") return null;
201
+ if (action !== void 0 && action !== "output" && action !== "kill" && action !== "agent-write") return null;
201
202
  return {
202
203
  id: decodeURIComponent(parts[0]),
203
204
  shells: true,
@@ -786,6 +787,8 @@ const HOST_ONLY_KEYS = new Set(Object.keys({
786
787
  sessionInfoFn: true,
787
788
  peers: true,
788
789
  shells: true,
790
+ shellAgentWrite: true,
791
+ createdByOperator: true,
789
792
  connectFn: true,
790
793
  codexHome: true,
791
794
  codexPathOverride: true,
@@ -1114,7 +1117,9 @@ const EPHEMERAL_CONFIG_KEYS = [
1114
1117
  "extraOptions",
1115
1118
  "env",
1116
1119
  "peers",
1117
- "instructions"
1120
+ "instructions",
1121
+ "shells",
1122
+ "shellAgentWrite"
1118
1123
  ];
1119
1124
  function toDurableRecord(record) {
1120
1125
  const config = { ...record.config };
@@ -1543,6 +1548,68 @@ function handleProjectIcon(projects, req, res, cwd) {
1543
1548
  res.end(read.data);
1544
1549
  }
1545
1550
  //#endregion
1551
+ //#region src/services/shell-screen.ts
1552
+ var ShellScreen = class {
1553
+ #term;
1554
+ #closing;
1555
+ #final;
1556
+ constructor(cols, rows) {
1557
+ this.#term = new xtermHeadless.Terminal({
1558
+ cols,
1559
+ rows,
1560
+ scrollback: 0,
1561
+ allowProposedApi: true
1562
+ });
1563
+ }
1564
+ write(data) {
1565
+ this.#term?.write(data);
1566
+ }
1567
+ resize(cols, rows) {
1568
+ try {
1569
+ this.#term?.resize(cols, rows);
1570
+ } catch {}
1571
+ }
1572
+ async snapshot() {
1573
+ if (this.#closing) await this.#closing;
1574
+ if (this.#final !== void 0) return this.#final;
1575
+ const term = this.#term;
1576
+ if (!term) return "";
1577
+ await drained(term);
1578
+ return render(term);
1579
+ }
1580
+ async applicationCursorKeys() {
1581
+ const term = this.#term;
1582
+ if (!term) return false;
1583
+ await drained(term);
1584
+ return term.modes.applicationCursorKeysMode;
1585
+ }
1586
+ close() {
1587
+ const term = this.#term;
1588
+ if (!term || this.#closing) return;
1589
+ this.#term = void 0;
1590
+ this.#closing = drained(term).then(() => {
1591
+ this.#final = render(term);
1592
+ term.dispose();
1593
+ });
1594
+ }
1595
+ };
1596
+ async function renderScreen(raw, cols, rows) {
1597
+ const screen = new ShellScreen(cols, rows);
1598
+ screen.write(raw);
1599
+ screen.close();
1600
+ return screen.snapshot();
1601
+ }
1602
+ function drained(term) {
1603
+ return new Promise((resolve) => term.write("", resolve));
1604
+ }
1605
+ function render(term) {
1606
+ const buffer = term.buffer.active;
1607
+ const lines = [];
1608
+ for (let y = 0; y < term.rows; y++) lines.push(buffer.getLine(buffer.viewportY + y)?.translateToString(true) ?? "");
1609
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
1610
+ return lines.join("\n");
1611
+ }
1612
+ //#endregion
1546
1613
  //#region src/services/process-tree.ts
1547
1614
  const PS_ARGS = ["-eo", "pid=,ppid=,pgid="];
1548
1615
  const PS_TIMEOUT_MS = 2e3;
@@ -1687,18 +1754,147 @@ async function loadPty() {
1687
1754
  }
1688
1755
  return ptyModule;
1689
1756
  }
1690
- function createShellDirectory(registry) {
1757
+ function createShellDirectory(registry, deps = {}) {
1758
+ const take = async (from, shellId, options) => {
1759
+ const info = registry.get(from, shellId);
1760
+ if (!info) return;
1761
+ const view = options.view ?? (info.interactive ? "screen" : "lines");
1762
+ const fresh = options.since === void 0 ? void 0 : await freshText(from, shellId, info.bytes - options.since);
1763
+ if (view === "screen") {
1764
+ const text = await registry.output(from, shellId, { view: "screen" });
1765
+ if (text === void 0) return;
1766
+ const lines = text === "" ? 0 : text.split("\n").length;
1767
+ return {
1768
+ info,
1769
+ read: {
1770
+ view,
1771
+ text,
1772
+ lines,
1773
+ totalLines: lines,
1774
+ truncated: false
1775
+ },
1776
+ haystack: fresh ?? text
1777
+ };
1778
+ }
1779
+ const text = await registry.output(from, shellId, { view: "text" });
1780
+ if (text === void 0) return;
1781
+ return {
1782
+ info,
1783
+ read: shellTail(text, clampShellTail(options.tail)),
1784
+ haystack: fresh ?? shellTail(text, SHELL_READ_MAX_LINES).text
1785
+ };
1786
+ };
1787
+ const freshText = async (from, shellId, bytes) => {
1788
+ if (bytes <= 0) return "";
1789
+ const raw = await registry.output(from, shellId, {
1790
+ view: "raw",
1791
+ tail: bytes
1792
+ });
1793
+ return raw === void 0 ? "" : ttyText(raw);
1794
+ };
1795
+ const settle = async (from, shellId, options) => {
1796
+ const needles = options.waitFor ?? [];
1797
+ const started = Date.now();
1798
+ const timeoutMs = Math.max(0, Math.min(options.timeoutMs ?? SHELL_WAIT_DEFAULT_MS, SHELL_WAIT_MAX_MS));
1799
+ for (;;) {
1800
+ const taken = await take(from, shellId, options);
1801
+ if (!taken) return;
1802
+ const result = {
1803
+ shell: shellSummary(taken.info),
1804
+ ...taken.read
1805
+ };
1806
+ if (needles.length === 0) return result;
1807
+ const ms = Date.now() - started;
1808
+ const match = needles.find((needle) => taken.haystack.includes(needle));
1809
+ if (match !== void 0) return {
1810
+ ...result,
1811
+ wait: {
1812
+ outcome: "matched",
1813
+ match,
1814
+ ms
1815
+ }
1816
+ };
1817
+ if (taken.info.status !== "running") return {
1818
+ ...result,
1819
+ wait: {
1820
+ outcome: "exited",
1821
+ ms
1822
+ }
1823
+ };
1824
+ if (ms >= timeoutMs) return {
1825
+ ...result,
1826
+ wait: {
1827
+ outcome: "timeout",
1828
+ ms
1829
+ }
1830
+ };
1831
+ await registry.changed(from, shellId, timeoutMs - ms);
1832
+ }
1833
+ };
1834
+ const writable = (from, shellId) => {
1835
+ const info = registry.get(from, shellId);
1836
+ if (!info) return;
1837
+ if (!agentMayWrite(info, from)) throw new Error(shellOwnershipRefusal(shellId));
1838
+ return info;
1839
+ };
1691
1840
  return {
1692
1841
  list: async (from) => registry.list(from).map(shellSummary),
1693
- read: async (from, shellId, options) => {
1694
- const info = registry.get(from, shellId);
1842
+ read: (from, shellId, options) => settle(from, shellId, { ...options }),
1843
+ run: async (from, options) => {
1844
+ const runner = deps.runnerFor?.(from);
1845
+ if (!runner?.queueLocalCommand) throw new Error(SHELL_REFUSAL);
1846
+ const { shell, source } = await registry.spawn({
1847
+ runner,
1848
+ command: options.command,
1849
+ owner: "agent"
1850
+ });
1851
+ try {
1852
+ runner.queueLocalCommand(source);
1853
+ } catch (error) {
1854
+ registry.kill(from, shell.id);
1855
+ throw error;
1856
+ }
1857
+ const result = await settle(from, shell.id, {
1858
+ waitFor: options.waitFor,
1859
+ timeoutMs: options.timeoutMs
1860
+ });
1861
+ if (!result) throw new Error(`shell ${shell.id} vanished as it started`);
1862
+ return result;
1863
+ },
1864
+ write: async (from, shellId, options) => {
1865
+ const info = writable(from, shellId);
1695
1866
  if (!info) return;
1696
- const text = await registry.output(from, shellId, { view: "text" });
1697
- if (text === void 0) return;
1698
- return {
1867
+ if (info.status !== "running") throw new Error(`shell ${shellId} has already ended; there is nothing to type into`);
1868
+ const application = await registry.applicationCursorKeys(from, shellId);
1869
+ const data = (options.data ?? "") + encodeShellKeys(options.keys ?? [], { applicationCursorKeys: application });
1870
+ if (data.length === 0) throw new Error("nothing to write: give data, keys or both");
1871
+ const since = registry.get(from, shellId)?.bytes ?? info.bytes;
1872
+ registry.write(from, shellId, data);
1873
+ return settle(from, shellId, {
1874
+ waitFor: options.waitFor,
1875
+ timeoutMs: options.timeoutMs,
1876
+ since
1877
+ });
1878
+ },
1879
+ kill: async (from, shellId) => {
1880
+ const info = writable(from, shellId);
1881
+ if (!info) return;
1882
+ if (info.status !== "running") return {
1699
1883
  shell: shellSummary(info),
1700
- ...shellTail(text, clampShellTail(options?.tail))
1884
+ killed: false
1885
+ };
1886
+ const killed = registry.kill(from, shellId, "killed") ?? info;
1887
+ return {
1888
+ shell: shellSummary(killed),
1889
+ killed: true
1701
1890
  };
1891
+ },
1892
+ grant: async (from, shellId) => {
1893
+ const info = registry.get(from, shellId);
1894
+ if (!info) return;
1895
+ if (info.owner === "agent") return shellSummary(info);
1896
+ const granted = registry.setAgentWrite(from, shellId, true);
1897
+ return granted ? shellSummary(granted) : void 0;
1702
1898
  }
1703
1899
  };
1704
1900
  }
@@ -1861,6 +2057,7 @@ function createShellRegistry(options) {
1861
2057
  entry.info.status = "exited";
1862
2058
  entry.info.endedAt = Date.now();
1863
2059
  entry.info.endReason = reason;
2060
+ delete entry.info.agentWrite;
1864
2061
  clearTimeout(entry.clock);
1865
2062
  clearTimeout(entry.notify);
1866
2063
  clearInterval(entry.tailTimer);
@@ -1868,6 +2065,7 @@ function createShellRegistry(options) {
1868
2065
  entry.child = void 0;
1869
2066
  entry.pid = void 0;
1870
2067
  if (entry.artifact instanceof LiveArtifact) entry.artifact.close();
2068
+ entry.screen?.close();
1871
2069
  for (const sink of entry.sinks) try {
1872
2070
  sink.end(reason);
1873
2071
  } catch {}
@@ -1959,6 +2157,7 @@ function createShellRegistry(options) {
1959
2157
  sinks: /* @__PURE__ */ new Set(),
1960
2158
  listeners: /* @__PURE__ */ new Set()
1961
2159
  };
2160
+ entry.screen = new ShellScreen(info.cols, info.rows);
1962
2161
  state.entries.set(id, entry);
1963
2162
  const env = shellChildEnv(process.env);
1964
2163
  env.TERM = TERM;
@@ -1988,6 +2187,15 @@ function createShellRegistry(options) {
1988
2187
  if (entry.info.status !== "running") return;
1989
2188
  const changed = artifact.append(data);
1990
2189
  entry.info.bytes = artifact.bytes;
2190
+ entry.screen?.write(data);
2191
+ if (!entry.info.interactive) {
2192
+ const scanned = (entry.redrawCarry ?? "") + data;
2193
+ if (ttyRedraws(scanned)) {
2194
+ entry.info.interactive = true;
2195
+ entry.redrawCarry = void 0;
2196
+ persist(state);
2197
+ } else entry.redrawCarry = scanned.slice(-TTY_REDRAW_CARRY);
2198
+ }
1991
2199
  if (changed) {
1992
2200
  if (artifact.capped && !entry.info.capped) {
1993
2201
  entry.info.capped = true;
@@ -2060,6 +2268,22 @@ function createShellRegistry(options) {
2060
2268
  kill(state, entry, reason);
2061
2269
  return { ...entry.info };
2062
2270
  },
2271
+ setAgentWrite: (sessionId, shellId, enabled) => {
2272
+ const state = sessions.get(sessionId);
2273
+ const entry = state?.entries.get(shellId);
2274
+ if (!state || !entry) return;
2275
+ if (enabled) {
2276
+ if (entry.info.owner !== "user") throw new Error(`shell ${shellId} was started by the agent, which may already type into it`);
2277
+ if (entry.info.status !== "running") throw new Error(`shell ${shellId} has already ended; there is nothing to grant`);
2278
+ }
2279
+ if (entry.info.agentWrite === true !== enabled) {
2280
+ if (enabled) entry.info.agentWrite = true;
2281
+ else delete entry.info.agentWrite;
2282
+ persist(state);
2283
+ fire(entry);
2284
+ }
2285
+ return { ...entry.info };
2286
+ },
2063
2287
  killAll: (reason) => {
2064
2288
  stopped = true;
2065
2289
  if (sweeper) {
@@ -2128,16 +2352,36 @@ function createShellRegistry(options) {
2128
2352
  } catch {}
2129
2353
  entry.info.cols = next.cols;
2130
2354
  entry.info.rows = next.rows;
2355
+ entry.screen?.resize(next.cols, next.rows);
2131
2356
  }
2132
2357
  return next;
2133
2358
  },
2134
2359
  output: async (sessionId, shellId, query) => {
2135
2360
  const entry = find(sessionId, shellId);
2136
2361
  if (!entry) return;
2362
+ if (query.view === "screen") {
2363
+ if (entry.screen) return entry.screen.snapshot();
2364
+ return renderScreen((await entry.artifact.replay(SHELL_ATTACH_REPLAY_BYTES)).toString("utf8"), entry.info.cols, entry.info.rows);
2365
+ }
2137
2366
  if (query.tail === void 0 && query.view === "text") return entry.artifact.textView();
2138
2367
  const text = (await entry.artifact.read(query.tail)).toString("utf8");
2139
2368
  return query.view === "raw" ? text : ttyText(text);
2140
2369
  },
2370
+ applicationCursorKeys: (sessionId, shellId) => find(sessionId, shellId)?.screen?.applicationCursorKeys() ?? Promise.resolve(false),
2371
+ changed: (sessionId, shellId, timeoutMs) => {
2372
+ const entry = find(sessionId, shellId);
2373
+ if (!entry || entry.info.status !== "running") return Promise.resolve();
2374
+ return new Promise((resolve) => {
2375
+ const done = () => {
2376
+ clearTimeout(timer);
2377
+ entry.listeners.delete(done);
2378
+ resolve();
2379
+ };
2380
+ const timer = setTimeout(done, Math.max(0, timeoutMs));
2381
+ timer.unref();
2382
+ entry.listeners.add(done);
2383
+ });
2384
+ },
2141
2385
  hydrate: async () => {
2142
2386
  if (dir) {
2143
2387
  let names = [];
@@ -2529,6 +2773,7 @@ async function readRange(path, start, end) {
2529
2773
  }
2530
2774
  //#endregion
2531
2775
  //#region src/routes/shells.ts
2776
+ const AGENT_WRITE_BODY_MAX = 1024;
2532
2777
  async function handleShells(ctx, req, res, route, runner, operator) {
2533
2778
  const { shells } = ctx;
2534
2779
  const sessionId = route.id;
@@ -2565,13 +2810,52 @@ async function handleShells(ctx, req, res, route, runner, operator) {
2565
2810
  json(res, 200, { shell });
2566
2811
  return;
2567
2812
  }
2813
+ if (route.shellAction === "agent-write") {
2814
+ if (req.method !== "POST") {
2815
+ json(res, 405, { error: "method not allowed" });
2816
+ return;
2817
+ }
2818
+ if (!runner || !shellPermitted(shells, runner, operator)) {
2819
+ json(res, 403, { error: SHELL_REFUSAL });
2820
+ return;
2821
+ }
2822
+ let body;
2823
+ try {
2824
+ body = await readJsonBody(req, AGENT_WRITE_BODY_MAX);
2825
+ } catch {
2826
+ json(res, 400, { error: "invalid JSON body" });
2827
+ return;
2828
+ }
2829
+ if (typeof body.enabled !== "boolean") {
2830
+ json(res, 400, { error: "enabled must be a boolean" });
2831
+ return;
2832
+ }
2833
+ if (body.enabled && runner.info().shellAgentWrite === void 0) {
2834
+ json(res, 409, { error: "the agent has no shell write tools on this session (shell.agentWrite is read-only)" });
2835
+ return;
2836
+ }
2837
+ let shell;
2838
+ try {
2839
+ shell = shells.setAgentWrite(sessionId, route.shellId, body.enabled);
2840
+ } catch (error) {
2841
+ json(res, 409, { error: error instanceof Error ? error.message : String(error) });
2842
+ return;
2843
+ }
2844
+ if (!shell) {
2845
+ json(res, 404, { error: "shell not found" });
2846
+ return;
2847
+ }
2848
+ json(res, 200, { shell });
2849
+ return;
2850
+ }
2568
2851
  if (req.method !== "GET") {
2569
2852
  json(res, 405, { error: "method not allowed" });
2570
2853
  return;
2571
2854
  }
2572
2855
  if (route.shellAction === "output") {
2573
2856
  const url = new URL(req.url ?? "/", "http://internal");
2574
- const view = url.searchParams.get("view") === "raw" ? "raw" : "text";
2857
+ const requested = url.searchParams.get("view");
2858
+ const view = requested === "raw" || requested === "screen" ? requested : "text";
2575
2859
  const tailParam = url.searchParams.get("tail");
2576
2860
  const tail = tailParam === null ? void 0 : Number(tailParam);
2577
2861
  if (tail !== void 0 && (!Number.isInteger(tail) || tail <= 0)) {
@@ -2666,7 +2950,7 @@ async function handleSessions(ctx, req, res, route, auth) {
2666
2950
  json(res, vetted.status, { error: vetted.error });
2667
2951
  return;
2668
2952
  }
2669
- const runner = await factory.createRunner(factory.buildRunnerConfig(vetted.request));
2953
+ const runner = await factory.createRunner(factory.buildRunnerConfig(vetted.request, { operator: authSvc.isOperator(auth) }));
2670
2954
  factory.watchAuthSource(runner);
2671
2955
  json(res, 201, { session: projects.withProject(runner.info()) });
2672
2956
  return;
@@ -4393,6 +4677,12 @@ var SessionRegistry = class {
4393
4677
  };
4394
4678
  //#endregion
4395
4679
  //#region src/services/session-factory.ts
4680
+ function withPrincipal(config, operator) {
4681
+ return operator === void 0 ? config : {
4682
+ ...config,
4683
+ createdByOperator: operator
4684
+ };
4685
+ }
4396
4686
  function createSessionFactory(deps) {
4397
4687
  const { adapterFor, profiles, refs } = deps;
4398
4688
  const subscriptionNoticeShown = /* @__PURE__ */ new Set();
@@ -4472,14 +4762,15 @@ function createSessionFactory(deps) {
4472
4762
  ...config,
4473
4763
  defaultApprovalTimeoutMs: deps.approvalTimeoutMs
4474
4764
  };
4475
- const buildRunnerConfig = (req) => {
4765
+ const buildRunnerConfig = (req, principal) => {
4476
4766
  const profile = req.profile !== void 0 ? profiles.get(req.profile) : void 0;
4477
- if (!profile) return withApprovalDefault(withScope(deps.hostBuildRunnerConfig(req), req.scope));
4478
- const config = withApprovalDefault(withScope(deps.hostBuildRunnerConfig({
4767
+ const operator = principal?.operator ?? req.createdByOperator;
4768
+ if (!profile) return withApprovalDefault(withPrincipal(withScope(deps.hostBuildRunnerConfig(req), req.scope), operator));
4769
+ const config = withApprovalDefault(withPrincipal(withScope(deps.hostBuildRunnerConfig({
4479
4770
  ...req,
4480
4771
  model: req.model ?? profile.defaults?.model ?? profile.provider?.model,
4481
4772
  permissionMode: req.permissionMode ?? profile.defaults?.permissionMode
4482
- }), req.scope));
4773
+ }), req.scope), operator));
4483
4774
  if (engineOf(profile) !== "claude") return config;
4484
4775
  const base = config.env ?? process.env;
4485
4776
  const env = claudeSessionEnv(profile, base);
@@ -4505,9 +4796,15 @@ function createSessionFactory(deps) {
4505
4796
  const capabilities = profile?.capabilities ?? adapterFor(profile?.engine).capabilities;
4506
4797
  const config = {
4507
4798
  ...built,
4508
- ...deps.peers ? { peers: deps.peers } : {},
4509
- ...deps.shells && capabilities.hostCwd === true ? { shells: deps.shells } : {}
4799
+ ...deps.peers ? { peers: deps.peers } : {}
4510
4800
  };
4801
+ delete config.shells;
4802
+ delete config.shellAgentWrite;
4803
+ if (deps.shells && capabilities.hostCwd === true) {
4804
+ config.shells = deps.shells;
4805
+ const write = deps.shellAgentWrite ?? "read-only";
4806
+ if (write !== "read-only" && built.createdByOperator === true) config.shellAgentWrite = write;
4807
+ }
4511
4808
  if (config.instructions !== void 0 && capabilities.systemInstructions === false) throw new Error(`the ${engineOf(profile)} engine cannot deliver system instructions`);
4512
4809
  const runner = profile && isProviderProfile(profile) ? await deps.createEngineRunner({
4513
4810
  config,
@@ -4695,7 +4992,7 @@ function createWorkerServer(options = {}) {
4695
4992
  options: options.peers
4696
4993
  });
4697
4994
  if (peers) installPeerDirectory(peers);
4698
- installShellDirectory(shells ? createShellDirectory(shells) : void 0);
4995
+ installShellDirectory(shells ? createShellDirectory(shells, { runnerFor: (id) => refs.registry?.get(id) }) : void 0);
4699
4996
  const factory = createSessionFactory({
4700
4997
  adapterFor,
4701
4998
  profiles,
@@ -4707,6 +5004,7 @@ function createWorkerServer(options = {}) {
4707
5004
  requireApiKey: options.requireApiKey,
4708
5005
  peers: peers ? peerDirectoryHandle() : void 0,
4709
5006
  shells: shells ? shellDirectoryHandle() : void 0,
5007
+ shellAgentWrite: options.shell?.agentWrite,
4710
5008
  refs
4711
5009
  });
4712
5010
  const availability = new AvailabilityTracker({
@@ -5128,6 +5426,7 @@ async function createProviderRunner(ctx, options) {
5128
5426
  const backend = isPerCall ? (call) => {
5129
5427
  return options.executor(call) === "browser" ? "browser" : "server";
5130
5428
  } : options.executor === "browser" ? "browser" : "server";
5429
+ const shouldApprove = config.shellAgentWrite === void 0 ? options.shouldApprove : (call) => shellToolNeedsCard(call.toolName, config.shellAgentWrite) || options.shouldApprove?.(call) === true;
5131
5430
  return createEngineSession({
5132
5431
  config: {
5133
5432
  ...config,
@@ -5146,7 +5445,7 @@ async function createProviderRunner(ctx, options) {
5146
5445
  mcpTools: options.mcpTools,
5147
5446
  instructions: options.instructions,
5148
5447
  executionLimits: options.executionLimits,
5149
- shouldApprove: options.shouldApprove,
5448
+ shouldApprove,
5150
5449
  approvalTimeoutMs: config.defaultApprovalTimeoutMs === void 0 ? options.approvalTimeoutMs : config.defaultApprovalTimeoutMs,
5151
5450
  seedVfs: options.seedVfs
5152
5451
  });
@@ -5191,6 +5490,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
5191
5490
  };
5192
5491
  }
5193
5492
  //#endregion
5194
- export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SHELL_REFUSAL, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellRegistry, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord };
5493
+ export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SHELL_REFUSAL, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellDirectory, createShellRegistry, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord };
5195
5494
 
5196
5495
  //# sourceMappingURL=index.mjs.map