@workerdeck/server 2.12.0 → 2.13.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.
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, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SessionRunnerConfig, ToolExecutionCall, ToolExecutionResult, ToolExecutor, ToolSet } from "@workerdeck/core";
2
+ import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, CostLedgerState, EngineAdapter, EngineSessionOptions, HostToolDefinition, LanguageModel, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SessionInstructions, SessionRunnerConfig, 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 } from "@workerdeck/protocol";
5
5
  //#region src/services/bridge.d.ts
@@ -323,7 +323,7 @@ type ProviderRunnerOptions = {
323
323
  tools?: Record<string, HostToolDefinition>;
324
324
  mcp?: McpConnection;
325
325
  mcpTools?: ToolSet;
326
- instructions?: string;
326
+ instructions?: SessionInstructions;
327
327
  executionLimits?: {
328
328
  timeoutMs?: number;
329
329
  memoryLimitBytes?: number;
package/build/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { createServer } from "node:http";
2
2
  import { WebSocketServer } from "ws";
3
3
  import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SessionRunner, attachmentKind, checkClaudeAuth, createEngineSession, getEngineAdapter, installPeerDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines } from "@workerdeck/core";
4
4
  import { JobQueue } from "@workerdeck/queue";
5
- import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_COMMAND_MAX, costOfByModel, imagePartRef, isJobRun, mergeByModel, peerMentionKey, peerMentionSlug, scanPeerMentions, sessionState, setPricingOverrides, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
5
+ import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_COMMAND_MAX, costOfByModel, imagePartRef, isJobRun, mergeByModel, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, setPricingOverrides, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { constants as constants$1, homedir } from "node:os";
@@ -751,34 +751,89 @@ async function handleHostFiles(ctx, req, res, pathname) {
751
751
  }
752
752
  json(res, 404, { error: "not found" });
753
753
  }
754
- //#endregion
755
- //#region src/routes/create-vet.ts
756
- function vetCreateRequest(ctx, req, auth) {
754
+ const HOST_ONLY_KEYS = new Set(Object.keys({
755
+ epoch: true,
756
+ queryFn: true,
757
+ env: true,
758
+ pathToClaudeCodeExecutable: true,
759
+ extraOptions: true,
760
+ instructions: true,
761
+ defaultApprovalTimeoutMs: true,
762
+ backfillHistory: true,
763
+ historyFn: true,
764
+ sessionInfoFn: true,
765
+ peers: true,
766
+ connectFn: true,
767
+ codexHome: true,
768
+ codexPathOverride: true,
769
+ languageModel: true,
770
+ tools: true,
771
+ maxSteps: true,
772
+ executor: true,
773
+ executableTools: true,
774
+ vfs: true,
775
+ executionLimits: true,
776
+ executionBackend: true,
777
+ toolTitles: true,
778
+ shouldApprove: true,
779
+ resolveModel: true,
780
+ reportMcpServers: true,
781
+ onClose: true,
782
+ restore: true
783
+ }));
784
+ function vetCreateRequest(ctx, body, auth) {
785
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return {
786
+ ok: false,
787
+ status: 400,
788
+ error: "request body must be a JSON object"
789
+ };
790
+ const hostOnly = Object.keys(body).filter((key) => HOST_ONLY_KEYS.has(key));
791
+ if (hostOnly.length > 0) return {
792
+ ok: false,
793
+ status: 400,
794
+ error: `host-only runner config is not accepted on a session request: ${hostOnly.join(", ")} (set it from the gateway's buildRunnerConfig hook)`
795
+ };
796
+ const req = pickCreateSessionRequest(body);
757
797
  const { availability, factory } = ctx;
758
798
  const refusedScope = factory.applyScope(req, auth);
759
- if (refusedScope) return refusedScope;
799
+ if (refusedScope) return {
800
+ ok: false,
801
+ ...refusedScope
802
+ };
760
803
  const refused = factory.applyBypassPolicy(req);
761
804
  if (refused) return {
805
+ ok: false,
762
806
  status: 403,
763
807
  error: refused
764
808
  };
765
809
  const resolved = factory.resolveProfile(req.profile, auth.allowedProfiles);
766
810
  if (!resolved.ok) return {
811
+ ok: false,
767
812
  status: resolved.status,
768
813
  error: resolved.error
769
814
  };
770
815
  const unavailable = availability.checkAvailable(resolved.profile);
771
- if (unavailable) return unavailable;
816
+ if (unavailable) return {
817
+ ok: false,
818
+ ...unavailable
819
+ };
772
820
  const refusedCwd = factory.checkCwd(req, resolved.profile);
773
- if (refusedCwd) return refusedCwd;
821
+ if (refusedCwd) return {
822
+ ok: false,
823
+ ...refusedCwd
824
+ };
774
825
  const badRequest = factory.checkPermissionMode(req.permissionMode, resolved.profile) ?? factory.checkEngineGrants(req, resolved.profile);
775
826
  if (badRequest) return {
827
+ ok: false,
776
828
  status: 400,
777
829
  error: badRequest
778
830
  };
779
831
  factory.stripInertFields(req, resolved.profile);
780
832
  req.profile = resolved.profile?.name;
781
- return null;
833
+ return {
834
+ ok: true,
835
+ request: req
836
+ };
782
837
  }
783
838
  //#endregion
784
839
  //#region src/routes/jobs.ts
@@ -808,21 +863,28 @@ async function handleJobs(ctx, req, res, pathname, auth) {
808
863
  }
809
864
  if (req.method === "POST") {
810
865
  const body = await readJsonBody(req, ctx.maxBodyBytes);
811
- if (!body.session || typeof body.session !== "object") {
866
+ if (!body?.session || typeof body.session !== "object") {
812
867
  json(res, 400, { error: "session is required" });
813
868
  return;
814
869
  }
815
- if (!body.session.prompt || typeof body.session.prompt !== "string") {
870
+ const prompt = body.session.prompt;
871
+ if (!prompt || typeof prompt !== "string") {
816
872
  json(res, 400, { error: "session.prompt is required" });
817
873
  return;
818
874
  }
819
- const refusal = vetCreateRequest(ctx, body.session, auth);
820
- if (refusal) {
821
- json(res, refusal.status, { error: refusal.error });
875
+ const vetted = vetCreateRequest(ctx, body.session, auth);
876
+ if (!vetted.ok) {
877
+ json(res, vetted.status, { error: vetted.error });
822
878
  return;
823
879
  }
824
880
  try {
825
- json(res, 201, { job: await queue.submit(body) });
881
+ json(res, 201, { job: await queue.submit({
882
+ ...body,
883
+ session: {
884
+ ...vetted.request,
885
+ prompt
886
+ }
887
+ }) });
826
888
  } catch (error) {
827
889
  json(res, 400, { error: error instanceof Error ? error.message : "invalid job" });
828
890
  }
@@ -1028,7 +1090,8 @@ const EPHEMERAL_CONFIG_KEYS = [
1028
1090
  "historyFn",
1029
1091
  "extraOptions",
1030
1092
  "env",
1031
- "peers"
1093
+ "peers",
1094
+ "instructions"
1032
1095
  ];
1033
1096
  function toDurableRecord(record) {
1034
1097
  const config = { ...record.config };
@@ -1517,13 +1580,12 @@ async function handleSessions(ctx, req, res, route, auth) {
1517
1580
  return;
1518
1581
  }
1519
1582
  if (req.method === "POST") {
1520
- const body = await readJsonBody(req, ctx.maxBodyBytes);
1521
- const refusal = vetCreateRequest(ctx, body, auth);
1522
- if (refusal) {
1523
- json(res, refusal.status, { error: refusal.error });
1583
+ const vetted = vetCreateRequest(ctx, await readJsonBody(req, ctx.maxBodyBytes), auth);
1584
+ if (!vetted.ok) {
1585
+ json(res, vetted.status, { error: vetted.error });
1524
1586
  return;
1525
1587
  }
1526
- const runner = await factory.createRunner(factory.buildRunnerConfig(body));
1588
+ const runner = await factory.createRunner(factory.buildRunnerConfig(vetted.request));
1527
1589
  factory.watchAuthSource(runner);
1528
1590
  json(res, 201, { session: projects.withProject(runner.info()) });
1529
1591
  return;
@@ -1611,7 +1673,15 @@ async function handleSessions(ctx, req, res, route, auth) {
1611
1673
  json(res, 409, { error: "session is parked (it has no pending permission requests)" });
1612
1674
  return;
1613
1675
  }
1614
- if (!runner.resolvePermission(route.permissionId, body)) {
1676
+ const decision = body.behavior === "allow" ? {
1677
+ behavior: "allow",
1678
+ updatedInput: body.updatedInput
1679
+ } : {
1680
+ behavior: "deny",
1681
+ message: body.message,
1682
+ interrupt: body.interrupt
1683
+ };
1684
+ if (!runner.resolvePermission(route.permissionId, decision)) {
1615
1685
  json(res, 404, { error: "permission request not found (already resolved or expired)" });
1616
1686
  return;
1617
1687
  }
@@ -2742,7 +2812,6 @@ var ProfileService = class {
2742
2812
  if (!hasEngineRunnerFactory) return `profile '${p.name}' uses engine 'provider' but no \`createEngineRunner\` was provided to build one`;
2743
2813
  } else if (p.engine === "codex") {
2744
2814
  if (p.codexHome && !existsSync(p.codexHome)) return `profile '${p.name}' codexHome does not exist: ${p.codexHome}`;
2745
- if (p.session?.instructions) return `profile '${p.name}' declares session.instructions, which the codex engine cannot deliver - put instructions in the target repo’s AGENTS.md instead`;
2746
2815
  } else if (!p.configDir || !existsSync(p.configDir)) return `profile '${p.name}' configDir does not exist: ${p.configDir}`;
2747
2816
  if (disableBypassPermissions && p.defaults?.permissionMode === "bypassPermissions") return `profile '${p.name}' defaults to bypassPermissions but disableBypassPermissions is set`;
2748
2817
  const fallbackMode = p.defaults?.permissionMode;
@@ -3403,6 +3472,8 @@ function createSessionFactory(deps) {
3403
3472
  const name = config.profile;
3404
3473
  const profile = name !== void 0 ? profiles.get(name) : void 0;
3405
3474
  if (name !== void 0 && !profile) throw new Error(`unknown profile: ${name}`);
3475
+ const capabilities = profile?.capabilities ?? adapterFor(profile?.engine).capabilities;
3476
+ if (config.instructions !== void 0 && capabilities.systemInstructions === false) throw new Error(`the ${engineOf(profile)} engine cannot deliver system instructions`);
3406
3477
  const runner = profile && isProviderProfile(profile) ? await deps.createEngineRunner({
3407
3478
  config,
3408
3479
  profile,