@loucompanion/forge-bridge 0.1.3-dev.0b2f81d660c0 → 0.1.3-dev.0bd9fb991b79

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 (51) hide show
  1. package/README.md +44 -0
  2. package/dist/bridge/bridge.base.d.ts +5 -1
  3. package/dist/bridge/bridge.service.d.ts +40 -2
  4. package/dist/bridge/bridge.service.js +564 -14
  5. package/dist/bridge/bridge.types.d.ts +110 -1
  6. package/dist/cli/cli.help.js +12 -0
  7. package/dist/cli/cli.service.d.ts +12 -0
  8. package/dist/cli/cli.service.js +168 -1
  9. package/dist/sandbox/command-runner.d.ts +4 -0
  10. package/dist/sandbox/command-runner.js +47 -0
  11. package/dist/sandbox/contract.d.ts +5 -0
  12. package/dist/sandbox/contract.js +276 -0
  13. package/dist/sandbox/event-budget.d.ts +11 -0
  14. package/dist/sandbox/event-budget.js +27 -0
  15. package/dist/sandbox/image-policy.d.ts +35 -0
  16. package/dist/sandbox/image-policy.js +247 -0
  17. package/dist/sandbox/index.d.ts +5 -0
  18. package/dist/sandbox/index.js +5 -0
  19. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  20. package/dist/sandbox/lease-watchdog.js +67 -0
  21. package/dist/sandbox/preflight.d.ts +40 -0
  22. package/dist/sandbox/preflight.js +256 -0
  23. package/dist/sandbox/process.d.ts +20 -0
  24. package/dist/sandbox/process.js +124 -0
  25. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  26. package/dist/sandbox/provider-auth/codex.js +212 -0
  27. package/dist/sandbox/repository-lock.d.ts +5 -0
  28. package/dist/sandbox/repository-lock.js +83 -0
  29. package/dist/sandbox/runtime.d.ts +234 -0
  30. package/dist/sandbox/runtime.js +1773 -0
  31. package/dist/sandbox/sandbox.types.d.ts +83 -0
  32. package/dist/sandbox/sandbox.types.js +1 -0
  33. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  34. package/dist/sandbox/worker-supervisor.js +36 -0
  35. package/dist/sandbox/worker.d.ts +1 -0
  36. package/dist/sandbox/worker.js +228 -0
  37. package/dist/sandbox/workspace.service.d.ts +12 -0
  38. package/dist/sandbox/workspace.service.js +49 -0
  39. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  40. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  41. package/dist/session/provider-cli/provider-cli.deterministic.js +20 -9
  42. package/dist/session/provider-cli/provider-cli.prompts.d.ts +2 -0
  43. package/dist/session/provider-cli/provider-cli.prompts.js +31 -0
  44. package/dist/session/provider-cli/provider-cli.service.d.ts +3 -0
  45. package/dist/session/provider-cli/provider-cli.service.js +72 -15
  46. package/dist/session/session.service.js +14 -7
  47. package/dist/session/session.types.d.ts +41 -1
  48. package/dist/session/session.types.js +119 -2
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +4 -2
@@ -5,7 +5,7 @@ import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-inject
5
5
  import { completionPhaseFromDispatch, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion } from "../phase-completion.js";
6
6
  import { cleanupCompletionTools, commitWorktreeChanges, completionPhaseFromArtifact, emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitDecomposeGateArtifacts, emitDraftGateArtifacts, emitPhaseCompletionEvent, emitPhaseGateEvents, emitQaArtifactRecorded, emitQaStructuredEvents, enforceQaBrowserLaunchTargetGate, LiveArchitectArtifactEmitter, LiveCompletionArtifactEmitter, LiveQaArtifactEmitter, readCompletionArtifactsForRun, readQaCompletionArtifactBundle, summarizeStderr, waitForCompletionArtifact } from "./provider-cli.artifacts.js";
7
7
  import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
8
- import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
8
+ import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
9
9
  import { StreamingProviderProcessRunner } from "./provider-cli.process.js";
10
10
  import { emitProviderUsageEvent, providerProcessEnv } from "./provider-cli.runtime.js";
11
11
  import { clip, createProviderOutputState, parseJsonLine } from "./provider-output-state.js";
@@ -13,6 +13,45 @@ export { DeterministicProviderCliService } from "./provider-cli.deterministic.js
13
13
  export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
14
14
  export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, StreamingProviderProcessRunner } from "./provider-cli.process.js";
15
15
  export { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
16
+ async function pushWorktreeBranch(git, worktreePath, branchName, emit) {
17
+ try {
18
+ await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
19
+ return;
20
+ }
21
+ catch (err) {
22
+ if (!isNonFastForwardPushError(err)) {
23
+ throw err;
24
+ }
25
+ await emit?.({
26
+ role: "runner",
27
+ kind: "checkpoint",
28
+ content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
29
+ eventType: "worktree_branch_push_rebase"
30
+ });
31
+ try {
32
+ await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
33
+ await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
34
+ await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
35
+ }
36
+ catch (retryErr) {
37
+ throw new Error(`git push origin HEAD:${branchName} was rejected because the remote branch moved, and automatic fetch/rebase failed: ${errorMessage(retryErr)}`, { cause: retryErr });
38
+ }
39
+ }
40
+ }
41
+ function remoteTrackingRefspec(branchName) {
42
+ return `refs/heads/${branchName}:refs/remotes/origin/${branchName}`;
43
+ }
44
+ function isNonFastForwardPushError(err) {
45
+ const message = errorMessage(err).toLowerCase();
46
+ return message.includes("non-fast-forward")
47
+ || message.includes("fetch first")
48
+ || message.includes("tip of your current branch is behind")
49
+ || message.includes("updates were rejected because the tip of your current branch is behind")
50
+ || message.includes("stale info");
51
+ }
52
+ function errorMessage(err) {
53
+ return err instanceof Error ? err.message : String(err);
54
+ }
16
55
  const fakeWorkerModel = "server-local-fake-worker";
17
56
  export class RoutingProviderCliService {
18
57
  realProvider;
@@ -31,6 +70,11 @@ export class RoutingProviderCliService {
31
70
  ? await this.deterministicProvider.runArchitect(context)
32
71
  : await this.realProvider.runArchitect(context);
33
72
  }
73
+ async runCiRepair(context) {
74
+ return isFakeWorkerModel(context.dispatch.execution.model)
75
+ ? await this.deterministicProvider.runCiRepair(context)
76
+ : await this.realProvider.runCiRepair(context);
77
+ }
34
78
  async runConflictResolve(context) {
35
79
  return isFakeWorkerModel(context.dispatch.execution.model)
36
80
  ? await this.deterministicProvider.runConflictResolve(context)
@@ -295,7 +339,7 @@ export class ProcessBackedProviderCliService {
295
339
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
296
340
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
297
341
  if (developCycle || debugCycle || phase === "implementation") {
298
- await this.git.run(worktree.worktreePath, ["push", "origin", `HEAD:${worktree.branchName}`]);
342
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
299
343
  await context.emit({
300
344
  role: "runner",
301
345
  kind: "checkpoint",
@@ -527,19 +571,28 @@ export class ProcessBackedProviderCliService {
527
571
  artifacts: [qaArtifact]
528
572
  };
529
573
  }
574
+ async runCiRepair(context) {
575
+ return await this.runPullRequestRepair(context, "ci_repair");
576
+ }
530
577
  async runConflictResolve(context) {
578
+ return await this.runPullRequestRepair(context, "conflict_resolve");
579
+ }
580
+ async runPullRequestRepair(context, route) {
531
581
  const { dispatch, worktree } = context;
532
- const addressComments = isPullRequestCommentsDispatch(dispatch);
582
+ const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
583
+ const addressComments = !repairChecks && isPullRequestCommentsDispatch(dispatch);
533
584
  const command = buildProviderCliCommand(dispatch, {
534
585
  worktreePath: worktree.worktreePath,
535
- prompt: addressComments
536
- ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
537
- : buildConflictResolvePrompt(dispatch, worktree.branchName)
586
+ prompt: repairChecks
587
+ ? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
588
+ : addressComments
589
+ ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
590
+ : buildConflictResolvePrompt(dispatch, worktree.branchName)
538
591
  });
539
592
  await context.emit({
540
593
  role: "runner",
541
594
  kind: "checkpoint",
542
- content: `Starting provider CLI ${addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
595
+ content: `Starting provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
543
596
  eventType: "provider_cli_started"
544
597
  });
545
598
  const outputState = createProviderOutputState(dispatch.execution.provider);
@@ -560,20 +613,24 @@ export class ProcessBackedProviderCliService {
560
613
  throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
561
614
  }
562
615
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
563
- await this.git.run(worktree.worktreePath, ["push", "origin", `HEAD:${worktree.branchName}`]);
616
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
564
617
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
565
618
  await context.emit({
566
619
  role: "assistant",
567
620
  kind: "message",
568
- content: outputState.finalMessage || (addressComments
569
- ? `${dispatch.execution.provider} CLI addressed pull request comments for ${worktree.branchName}.`
570
- : `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`),
571
- eventType: addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
621
+ content: outputState.finalMessage || (repairChecks
622
+ ? `${dispatch.execution.provider} CLI repaired pull request checks for ${worktree.branchName}.`
623
+ : addressComments
624
+ ? `${dispatch.execution.provider} CLI addressed pull request comments for ${worktree.branchName}.`
625
+ : `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`),
626
+ eventType: repairChecks ? "pr_checks_complete" : addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
572
627
  });
573
628
  return {
574
- summary: addressComments
575
- ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
576
- : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
629
+ summary: repairChecks
630
+ ? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
631
+ : addressComments
632
+ ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
633
+ : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
577
634
  };
578
635
  }
579
636
  }
@@ -1,10 +1,13 @@
1
1
  import { redactSecrets } from "../shared/redaction/redaction.js";
2
2
  import { RoutingProviderCliService } from "./provider-cli/provider-cli.service.js";
3
3
  import { GitWorktreeService } from "../worktree/worktree.service.js";
4
+ import { SandboxWorkspaceService } from "../sandbox/workspace.service.js";
4
5
  export class BridgeSessionService {
5
6
  worktrees;
6
7
  providerCli;
7
- constructor(worktrees = new GitWorktreeService(), providerCli = new RoutingProviderCliService()) {
8
+ constructor(worktrees = process.env.FORGE_SANDBOX_ID
9
+ ? new SandboxWorkspaceService()
10
+ : new GitWorktreeService(), providerCli = new RoutingProviderCliService()) {
8
11
  this.worktrees = worktrees;
9
12
  this.providerCli = providerCli;
10
13
  }
@@ -75,10 +78,12 @@ export class BridgeSessionService {
75
78
  }, redactionSecrets);
76
79
  }
77
80
  throwIfAborted(signal);
78
- const lane = dispatch.execution.lane.toLowerCase();
79
- const role = dispatch.execution.role.toLowerCase();
81
+ const lane = dispatch.execution.lane.trim().toLowerCase();
82
+ const role = dispatch.execution.role.trim().toLowerCase();
83
+ const cycleKind = dispatch.cycleRun.kind.trim().toLowerCase();
80
84
  const isQa = role === "qa" || lane === "qa";
81
- const isArchitect = role === "architect" || lane === "architect" || dispatch.cycleRun.kind.toLowerCase() === "architect";
85
+ const isArchitect = role === "architect" || lane === "architect" || cycleKind === "architect";
86
+ const isCiRepair = role === "ci_repair" || lane === "ci_repair" || cycleKind === "pr_checks";
82
87
  const isConflictResolve = role === "conflict_resolve" || lane === "conflict_resolve";
83
88
  const context = {
84
89
  dispatch: providerDispatch,
@@ -90,9 +95,11 @@ export class BridgeSessionService {
90
95
  ? await this.providerCli.runQa(context)
91
96
  : isArchitect
92
97
  ? await this.providerCli.runArchitect(context)
93
- : isConflictResolve
94
- ? await this.providerCli.runConflictResolve(context)
95
- : await this.providerCli.runImplementation(context);
98
+ : isCiRepair
99
+ ? await this.providerCli.runCiRepair(context)
100
+ : isConflictResolve
101
+ ? await this.providerCli.runConflictResolve(context)
102
+ : await this.providerCli.runImplementation(context);
96
103
  throwIfAborted(signal);
97
104
  const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
98
105
  return redactSecrets({
@@ -1,5 +1,5 @@
1
1
  export interface RunOnceDispatchPacket {
2
- readonly schemaVersion: 1;
2
+ readonly schemaVersion: 1 | 2;
3
3
  readonly userId: string;
4
4
  readonly workItem: DispatchWorkItem;
5
5
  readonly repo: DispatchRepo;
@@ -8,8 +8,10 @@ export interface RunOnceDispatchPacket {
8
8
  readonly lease: DispatchLease;
9
9
  readonly execution: DispatchExecution;
10
10
  readonly serverLocal: DispatchServerLocalConfig;
11
+ readonly sandbox?: DispatchSandboxContext | null;
11
12
  readonly pullRequest?: DispatchPullRequestContext | null;
12
13
  readonly apiKeySecretGrant?: ApiKeySecretGrant | null;
14
+ readonly apiKeySecretGrantHandle?: ApiKeySecretGrantHandle | null;
13
15
  readonly artifacts: readonly DispatchResolvedArtifact[];
14
16
  readonly warmPathOverrides?: readonly string[];
15
17
  readonly qaBrowser?: DispatchQaBrowserContext | null;
@@ -22,6 +24,39 @@ export interface RunOnceDispatchPacket {
22
24
  readonly designerRefs?: DispatchDesignerRefHandoffContext | null;
23
25
  readonly draft?: DispatchDraftPrompt | null;
24
26
  }
27
+ export interface DispatchSandboxContext {
28
+ readonly dispatchAttemptId: string;
29
+ readonly executionLeaseId: string;
30
+ readonly runnerInstanceId: string;
31
+ readonly executionMode: "host" | "docker";
32
+ readonly executionPolicy: "host_allowed" | "sandbox_preferred" | "sandbox_required";
33
+ readonly networkPolicy: DispatchSandboxNetworkPolicy;
34
+ readonly requestedResources: DispatchSandboxResourceRequest;
35
+ readonly runtime?: string | null;
36
+ readonly sandboxConfigCommitSha?: string | null;
37
+ readonly inputCommitSha?: string | null;
38
+ readonly contractDigest?: string | null;
39
+ readonly imageDigest?: string | null;
40
+ readonly publicationMode: "bridge_push" | "shared_filesystem";
41
+ readonly publishGrantHandle?: DispatchPublishGrantHandle | null;
42
+ }
43
+ export interface DispatchPublishGrantHandle {
44
+ readonly grantId: string;
45
+ readonly branch: string;
46
+ readonly expiresAt: string;
47
+ }
48
+ export interface DispatchSandboxResourceRequest {
49
+ readonly sandboxSlots: number;
50
+ readonly cpuMillis: number;
51
+ readonly memoryBytes: number;
52
+ readonly workspaceBytes: number;
53
+ readonly pids: number;
54
+ readonly buildSlots: number;
55
+ }
56
+ export interface DispatchSandboxNetworkPolicy {
57
+ readonly mode: "offline" | "public_allowlist" | "open_public";
58
+ readonly allowedHosts: readonly string[];
59
+ }
25
60
  export interface DispatchDraftPrompt {
26
61
  readonly kind: string;
27
62
  readonly systemPrompt: string;
@@ -279,6 +314,11 @@ export interface ApiKeySecretGrant {
279
314
  readonly secret: string;
280
315
  readonly expiresAt: string;
281
316
  }
317
+ export interface ApiKeySecretGrantHandle {
318
+ readonly grantId: string;
319
+ readonly provider: string;
320
+ readonly expiresAt: string;
321
+ }
282
322
  export interface DispatchServerLocalConfig {
283
323
  readonly repoRoot: string;
284
324
  readonly cleanupWorktrees: boolean;
@@ -2,11 +2,11 @@ import path from "node:path";
2
2
  export function parseRunOnceDispatchPacket(value) {
3
3
  const packet = asObject(value, "dispatch packet");
4
4
  const schemaVersion = requiredNumber(packet, "schemaVersion");
5
- if (schemaVersion !== 1) {
5
+ if (schemaVersion !== 1 && schemaVersion !== 2) {
6
6
  throw new Error(`Unsupported dispatch schemaVersion: ${schemaVersion}`);
7
7
  }
8
8
  return {
9
- schemaVersion: 1,
9
+ schemaVersion,
10
10
  userId: requiredString(packet, "userId"),
11
11
  workItem: parseWorkItem(packet.workItem),
12
12
  repo: parseRepo(packet.repo),
@@ -15,8 +15,10 @@ export function parseRunOnceDispatchPacket(value) {
15
15
  lease: parseLease(packet.lease),
16
16
  execution: parseExecution(packet.execution),
17
17
  serverLocal: parseServerLocal(packet.serverLocal),
18
+ sandbox: parseSandbox(packet.sandbox, schemaVersion),
18
19
  pullRequest: parsePullRequest(packet.pullRequest),
19
20
  apiKeySecretGrant: parseApiKeySecretGrant(packet.apiKeySecretGrant),
21
+ apiKeySecretGrantHandle: parseApiKeySecretGrantHandle(packet.apiKeySecretGrantHandle),
20
22
  artifacts: parseArtifacts(packet.artifacts),
21
23
  warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides),
22
24
  qaBrowser: parseQaBrowser(packet.qaBrowser),
@@ -30,6 +32,104 @@ export function parseRunOnceDispatchPacket(value) {
30
32
  draft: parseDraftPrompt(packet.draft)
31
33
  };
32
34
  }
35
+ function parseSandbox(value, schemaVersion) {
36
+ if (value === undefined || value === null) {
37
+ if (schemaVersion === 2) {
38
+ throw new Error("sandbox is required for dispatch schemaVersion 2.");
39
+ }
40
+ return null;
41
+ }
42
+ const sandbox = asObject(value, "sandbox");
43
+ const executionMode = requiredString(sandbox, "executionMode");
44
+ if (executionMode !== "host" && executionMode !== "docker") {
45
+ throw new Error("sandbox.executionMode must be host or docker.");
46
+ }
47
+ const executionPolicy = requiredString(sandbox, "executionPolicy");
48
+ if (executionPolicy !== "host_allowed" && executionPolicy !== "sandbox_preferred" && executionPolicy !== "sandbox_required") {
49
+ throw new Error("sandbox.executionPolicy is invalid.");
50
+ }
51
+ return {
52
+ dispatchAttemptId: requiredString(sandbox, "dispatchAttemptId"),
53
+ executionLeaseId: requiredString(sandbox, "executionLeaseId"),
54
+ runnerInstanceId: requiredString(sandbox, "runnerInstanceId"),
55
+ executionMode,
56
+ executionPolicy,
57
+ networkPolicy: parseSandboxNetworkPolicy(sandbox.networkPolicy),
58
+ requestedResources: parseSandboxResourceRequest(sandbox.requestedResources),
59
+ runtime: optionalString(sandbox, "runtime"),
60
+ sandboxConfigCommitSha: optionalString(sandbox, "sandboxConfigCommitSha"),
61
+ inputCommitSha: optionalString(sandbox, "inputCommitSha"),
62
+ contractDigest: optionalString(sandbox, "contractDigest"),
63
+ imageDigest: optionalString(sandbox, "imageDigest"),
64
+ publicationMode: parsePublicationMode(sandbox.publicationMode),
65
+ publishGrantHandle: parsePublishGrantHandle(sandbox.publishGrantHandle)
66
+ };
67
+ }
68
+ function parsePublicationMode(value) {
69
+ if (value === undefined || value === null)
70
+ return "shared_filesystem";
71
+ if (value !== "bridge_push" && value !== "shared_filesystem")
72
+ throw new Error("sandbox.publicationMode is invalid.");
73
+ return value;
74
+ }
75
+ function parsePublishGrantHandle(value) {
76
+ if (value === undefined || value === null)
77
+ return null;
78
+ const handle = asObject(value, "sandbox.publishGrantHandle");
79
+ const grantId = requiredString(handle, "grantId");
80
+ const branch = requiredString(handle, "branch");
81
+ const expiresAt = requiredString(handle, "expiresAt");
82
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(grantId)) {
83
+ throw new Error("sandbox.publishGrantHandle.grantId is invalid.");
84
+ }
85
+ if (branch.length > 240)
86
+ throw new Error("sandbox.publishGrantHandle.branch is too long.");
87
+ if (!Number.isFinite(Date.parse(expiresAt)))
88
+ throw new Error("sandbox.publishGrantHandle.expiresAt is invalid.");
89
+ return { grantId, branch, expiresAt };
90
+ }
91
+ function parseSandboxResourceRequest(value) {
92
+ if (value === undefined || value === null) {
93
+ return {
94
+ sandboxSlots: 1,
95
+ cpuMillis: 2000,
96
+ memoryBytes: 2 * 1024 ** 3,
97
+ workspaceBytes: 10 * 1024 ** 3,
98
+ pids: 512,
99
+ buildSlots: 1
100
+ };
101
+ }
102
+ const resources = asObject(value, "sandbox.requestedResources");
103
+ return {
104
+ sandboxSlots: boundedInteger(resources, "sandboxSlots", 1, 4),
105
+ cpuMillis: boundedInteger(resources, "cpuMillis", 100, 64_000),
106
+ memoryBytes: boundedInteger(resources, "memoryBytes", 128 * 1024 ** 2, 512 * 1024 ** 3),
107
+ workspaceBytes: boundedInteger(resources, "workspaceBytes", 1024 ** 3, 10 * 1024 ** 4),
108
+ pids: boundedInteger(resources, "pids", 32, 65_536),
109
+ buildSlots: boundedInteger(resources, "buildSlots", 0, 4)
110
+ };
111
+ }
112
+ function parseSandboxNetworkPolicy(value) {
113
+ if (value === undefined || value === null)
114
+ return { mode: "public_allowlist", allowedHosts: [] };
115
+ const policy = asObject(value, "sandbox.networkPolicy");
116
+ const mode = requiredString(policy, "mode");
117
+ if (mode !== "offline" && mode !== "public_allowlist" && mode !== "open_public") {
118
+ throw new Error("sandbox.networkPolicy.mode is invalid.");
119
+ }
120
+ const rawHosts = policy.allowedHosts ?? [];
121
+ if (!Array.isArray(rawHosts) || rawHosts.some((host) => typeof host !== "string")) {
122
+ throw new Error("sandbox.networkPolicy.allowedHosts must be a string array.");
123
+ }
124
+ const allowedHosts = [...new Set(rawHosts.map(host => host.trim().toLowerCase()).filter(Boolean))];
125
+ if (allowedHosts.length > 128 || allowedHosts.some(host => !/^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(host))) {
126
+ throw new Error("sandbox.networkPolicy.allowedHosts contains an invalid hostname.");
127
+ }
128
+ if (mode !== "public_allowlist" && allowedHosts.length > 0) {
129
+ throw new Error("sandbox.networkPolicy.allowedHosts requires public_allowlist mode.");
130
+ }
131
+ return { mode, allowedHosts };
132
+ }
33
133
  export function failureResult(summary, events = []) {
34
134
  return {
35
135
  schemaVersion: 1,
@@ -172,6 +272,16 @@ function parseApiKeySecretGrant(value) {
172
272
  expiresAt: requiredString(grant, "expiresAt")
173
273
  };
174
274
  }
275
+ function parseApiKeySecretGrantHandle(value) {
276
+ if (value === undefined || value === null)
277
+ return null;
278
+ const grant = asObject(value, "apiKeySecretGrantHandle");
279
+ return {
280
+ grantId: requiredString(grant, "grantId"),
281
+ provider: requiredString(grant, "provider"),
282
+ expiresAt: requiredString(grant, "expiresAt")
283
+ };
284
+ }
175
285
  function parseServerLocal(value) {
176
286
  const serverLocal = asObject(value, "serverLocal");
177
287
  return {
@@ -616,3 +726,10 @@ function requiredNumber(source, key) {
616
726
  }
617
727
  return value;
618
728
  }
729
+ function boundedInteger(source, key, minimum, maximum) {
730
+ const value = requiredNumber(source, key);
731
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
732
+ throw new Error(`${key} must be an integer from ${minimum} through ${maximum}.`);
733
+ }
734
+ return value;
735
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const forgeBridgeVersion = "0.1.3-dev.0b2f81d660c0";
1
+ export declare const forgeBridgeVersion = "0.1.3-dev.0bd9fb991b79";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.3-dev.0b2f81d660c0";
1
+ export const forgeBridgeVersion = "0.1.3-dev.0bd9fb991b79";
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@loucompanion/forge-bridge",
3
- "version": "0.1.3-dev.0b2f81d660c0",
3
+ "version": "0.1.3-dev.0bd9fb991b79",
4
4
  "description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "bin": {
8
+ "forge": "dist/cli/bin.js",
8
9
  "forge-bridge": "dist/cli/bin.js"
9
10
  },
10
11
  "main": "./dist/index.js",
@@ -43,7 +44,8 @@
43
44
  "lint": "eslint \"src/**/*.ts\" \"scripts/**/*.mjs\" \"vitest.config.ts\""
44
45
  },
45
46
  "dependencies": {
46
- "@microsoft/signalr": "^8.0.17"
47
+ "@microsoft/signalr": "^8.0.17",
48
+ "yaml": "^2.9.0"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@eslint/js": "^10.0.1",