@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.1ff37038366e
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/README.md +165 -1
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +9 -1
- package/dist/bridge/bridge.service.d.ts +129 -3
- package/dist/bridge/bridge.service.js +2427 -70
- package/dist/bridge/bridge.types.d.ts +218 -2
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +14 -2
- package/dist/cleanup/cleanup.service.js +496 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +55 -7
- package/dist/cli/cli.service.js +429 -219
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
- package/dist/runtime/attempt-process-supervisor.js +397 -0
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +20 -0
- package/dist/runtime/generated-output-reset.js +157 -0
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +191 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +51 -0
- package/dist/runtime/host-listener-inspector.js +385 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +58 -0
- package/dist/runtime/host-runtime-contract.js +813 -0
- package/dist/runtime/host-runtime-lease.d.ts +217 -0
- package/dist/runtime/host-runtime-lease.js +2487 -0
- package/dist/runtime/host-runtime-promotion.d.ts +32 -0
- package/dist/runtime/host-runtime-promotion.js +206 -0
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +285 -0
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +161 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +6 -0
- package/dist/sandbox/repository-lock.js +128 -0
- package/dist/sandbox/runtime.d.ts +302 -0
- package/dist/sandbox/runtime.js +2367 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +74 -0
- package/dist/session/managed-attempt-tools.d.ts +28 -0
- package/dist/session/managed-attempt-tools.js +1015 -0
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +27 -0
- package/dist/session/phase-completion.artifacts.js +829 -0
- package/dist/session/phase-completion.d.ts +53 -42
- package/dist/session/phase-completion.helpers.d.ts +117 -0
- package/dist/session/phase-completion.helpers.js +707 -0
- package/dist/session/phase-completion.js +125 -3007
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +233 -0
- package/dist/session/phase-completion.runtime.d.ts +49 -0
- package/dist/session/phase-completion.runtime.js +840 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +859 -0
- package/dist/session/phase-completion.validation.d.ts +38 -0
- package/dist/session/phase-completion.validation.js +918 -0
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
- package/dist/session/provider-cli/provider-cli.process.js +874 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +16 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +81 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
- package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
- package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
- package/dist/session/provider-cli/provider-output-state.js +817 -0
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +448 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +586 -59
- package/dist/session/session.types.d.ts +324 -3
- package/dist/session/session.types.js +939 -10
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +21 -1
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +330 -0
- package/dist/shared/process/process.d.ts +3 -0
- package/dist/shared/process/process.js +13 -7
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +4 -0
- package/dist/shared/provider-capabilities.js +88 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +47 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +82 -10
- package/dist/worktree/dependency-warmer.d.ts +53 -1
- package/dist/worktree/dependency-warmer.js +619 -11
- package/dist/worktree/index.d.ts +2 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +16 -3
- package/dist/worktree/worktree.service.js +334 -54
- package/dist/worktree/worktree.types.d.ts +9 -1
- package/package.json +12 -7
- package/runtime/windows-job-supervisor.ps1 +722 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
const GiB = 1024 ** 3;
|
|
6
|
+
const alignmentBytes = 512 * 1024 ** 2;
|
|
7
|
+
export const minimumWorkspaceEstimateBytes = 2 * GiB;
|
|
8
|
+
export const architectMinimumWorkspaceEstimateBytes = GiB;
|
|
9
|
+
const maximumBytes = 100 * GiB;
|
|
10
|
+
const footprintCacheLifetimeMs = 5 * 60_000;
|
|
11
|
+
const excludedDirectories = new Set([
|
|
12
|
+
".git",
|
|
13
|
+
".artifacts",
|
|
14
|
+
".forge-worktrees",
|
|
15
|
+
".local",
|
|
16
|
+
".next",
|
|
17
|
+
".nuget",
|
|
18
|
+
".pnpm-store",
|
|
19
|
+
".tmp",
|
|
20
|
+
".turbo",
|
|
21
|
+
"artifacts",
|
|
22
|
+
"bin",
|
|
23
|
+
"build",
|
|
24
|
+
"coverage",
|
|
25
|
+
"dist",
|
|
26
|
+
"node_modules",
|
|
27
|
+
"obj",
|
|
28
|
+
"testresults",
|
|
29
|
+
]);
|
|
30
|
+
const footprintCache = new Map();
|
|
31
|
+
export async function estimateWorkspaceBytesFromRepo(repoPath, hints = {}) {
|
|
32
|
+
const cacheKey = path.resolve(repoPath);
|
|
33
|
+
const cached = footprintCache.get(cacheKey);
|
|
34
|
+
const footprint = cached && Date.now() - cached.measuredAt < footprintCacheLifetimeMs
|
|
35
|
+
? cached.footprint
|
|
36
|
+
: await measureRepositoryWorkspace(cacheKey);
|
|
37
|
+
if (!cached || cached.footprint !== footprint) {
|
|
38
|
+
footprintCache.set(cacheKey, { measuredAt: Date.now(), footprint });
|
|
39
|
+
}
|
|
40
|
+
return calculateWorkspaceBytes({
|
|
41
|
+
...footprint,
|
|
42
|
+
hasBrowser: footprint.hasBrowser || hints.browser === true,
|
|
43
|
+
serviceCount: Math.max(footprint.serviceCount, Math.max(0, hints.serviceCount ?? 0)),
|
|
44
|
+
}, hints.role);
|
|
45
|
+
}
|
|
46
|
+
export function calculateWorkspaceBytes(footprint, role) {
|
|
47
|
+
const normalizedRole = role?.trim().toLowerCase() ?? "";
|
|
48
|
+
if (normalizedRole === "architect") {
|
|
49
|
+
// Architect owns a checkout and planning artifacts, not dependencies,
|
|
50
|
+
// build outputs, browser state, services, or application runtime output.
|
|
51
|
+
const estimate = architectMinimumWorkspaceEstimateBytes
|
|
52
|
+
+ Math.max(0, footprint.sourceBytes) * 2;
|
|
53
|
+
return Math.min(maximumBytes, Math.max(architectMinimumWorkspaceEstimateBytes, alignUp(estimate, alignmentBytes)));
|
|
54
|
+
}
|
|
55
|
+
let estimate = 2 * GiB + Math.max(0, footprint.sourceBytes) * 3;
|
|
56
|
+
if (footprint.hasNode)
|
|
57
|
+
estimate += 3 * GiB;
|
|
58
|
+
if (footprint.hasDotNet)
|
|
59
|
+
estimate += 2 * GiB;
|
|
60
|
+
if (footprint.hasBrowser)
|
|
61
|
+
estimate += 1536 * 1024 ** 2;
|
|
62
|
+
estimate += Math.max(0, footprint.serviceCount) * GiB;
|
|
63
|
+
if (["qa", "ci_repair", "pr_fix"].includes(normalizedRole)) {
|
|
64
|
+
estimate += GiB;
|
|
65
|
+
}
|
|
66
|
+
return Math.min(maximumBytes, Math.max(minimumWorkspaceEstimateBytes, alignUp(estimate, alignmentBytes)));
|
|
67
|
+
}
|
|
68
|
+
export async function measureRepositoryWorkspace(repoPath) {
|
|
69
|
+
return await measureGitWorkspace(repoPath) ?? await measureFilesystemFallback(repoPath);
|
|
70
|
+
}
|
|
71
|
+
async function measureGitWorkspace(repoPath) {
|
|
72
|
+
let sourceBytes = 0;
|
|
73
|
+
let hasNode = false;
|
|
74
|
+
let hasDotNet = false;
|
|
75
|
+
let hasBrowser = false;
|
|
76
|
+
let serviceCount = 0;
|
|
77
|
+
const child = spawn("git", ["-c", "core.quotepath=false", "ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
78
|
+
cwd: repoPath,
|
|
79
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
80
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
const closed = new Promise((resolve) => {
|
|
84
|
+
child.once("close", resolve);
|
|
85
|
+
child.once("error", () => resolve(null));
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
89
|
+
for await (const relativePath of lines) {
|
|
90
|
+
if (!relativePath.trim())
|
|
91
|
+
continue;
|
|
92
|
+
const file = await inspectWorkspaceFile(path.join(repoPath, relativePath));
|
|
93
|
+
if (!file)
|
|
94
|
+
continue;
|
|
95
|
+
sourceBytes += file.bytes;
|
|
96
|
+
hasNode ||= file.hasNode;
|
|
97
|
+
hasDotNet ||= file.hasDotNet;
|
|
98
|
+
hasBrowser ||= file.hasBrowser;
|
|
99
|
+
serviceCount = Math.max(serviceCount, file.serviceCount);
|
|
100
|
+
}
|
|
101
|
+
return await closed === 0 ? { sourceBytes, hasNode, hasDotNet, hasBrowser, serviceCount } : null;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
child.kill();
|
|
105
|
+
await closed;
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function measureFilesystemFallback(repoPath) {
|
|
110
|
+
let sourceBytes = 0;
|
|
111
|
+
let hasNode = false;
|
|
112
|
+
let hasDotNet = false;
|
|
113
|
+
let hasBrowser = false;
|
|
114
|
+
let serviceCount = 0;
|
|
115
|
+
const pending = [path.resolve(repoPath)];
|
|
116
|
+
while (pending.length > 0) {
|
|
117
|
+
const current = pending.pop();
|
|
118
|
+
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const entryPath = path.join(current, entry.name);
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
if (!excludedDirectories.has(entry.name.toLowerCase()))
|
|
123
|
+
pending.push(entryPath);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!entry.isFile())
|
|
127
|
+
continue;
|
|
128
|
+
const file = await inspectWorkspaceFile(entryPath);
|
|
129
|
+
if (!file)
|
|
130
|
+
continue;
|
|
131
|
+
sourceBytes += file.bytes;
|
|
132
|
+
hasNode ||= file.hasNode;
|
|
133
|
+
hasDotNet ||= file.hasDotNet;
|
|
134
|
+
hasBrowser ||= file.hasBrowser;
|
|
135
|
+
serviceCount = Math.max(serviceCount, file.serviceCount);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { sourceBytes, hasNode, hasDotNet, hasBrowser, serviceCount };
|
|
139
|
+
}
|
|
140
|
+
async function inspectWorkspaceFile(filePath) {
|
|
141
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
142
|
+
if (!stat?.isFile())
|
|
143
|
+
return null;
|
|
144
|
+
const name = path.basename(filePath).toLowerCase();
|
|
145
|
+
const sandbox = name === "forge.sandbox.yml" || name === "forge.sandbox.yaml";
|
|
146
|
+
return {
|
|
147
|
+
bytes: stat.size,
|
|
148
|
+
hasNode: ["package.json", "pnpm-lock.yaml", "package-lock.json", "yarn.lock"].includes(name),
|
|
149
|
+
hasDotNet: name.endsWith(".sln") || name.endsWith(".csproj") || name.endsWith(".fsproj"),
|
|
150
|
+
hasBrowser: name.startsWith("playwright.config"),
|
|
151
|
+
serviceCount: sandbox
|
|
152
|
+
? countSandboxServices(await fs.readFile(filePath, "utf8").catch(() => ""))
|
|
153
|
+
: 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function countSandboxServices(yaml) {
|
|
157
|
+
let inServices = false;
|
|
158
|
+
let count = 0;
|
|
159
|
+
for (const rawLine of yaml.split(/\r?\n/gu)) {
|
|
160
|
+
const line = rawLine.trim();
|
|
161
|
+
if (!line || line.startsWith("#"))
|
|
162
|
+
continue;
|
|
163
|
+
if (!/^\s/u.test(rawLine)) {
|
|
164
|
+
inServices = line.toLowerCase() === "services:";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (inServices && line.startsWith("-"))
|
|
168
|
+
count += 1;
|
|
169
|
+
}
|
|
170
|
+
return count;
|
|
171
|
+
}
|
|
172
|
+
function alignUp(value, alignment) {
|
|
173
|
+
return Math.ceil(value / alignment) * alignment;
|
|
174
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export class BoundedCommandRunner {
|
|
3
|
+
async run(command, args, options = {}) {
|
|
4
|
+
const maxOutputBytes = options.maxOutputBytes ?? 1024 * 1024;
|
|
5
|
+
return await new Promise((resolve, reject) => {
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const abort = () => controller.abort(options.signal?.reason ?? "command cancelled");
|
|
8
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
9
|
+
const timeout = options.timeoutMs
|
|
10
|
+
? setTimeout(() => controller.abort(`command exceeded ${options.timeoutMs}ms`), options.timeoutMs)
|
|
11
|
+
: null;
|
|
12
|
+
timeout?.unref?.();
|
|
13
|
+
const child = spawn(command, [...args], {
|
|
14
|
+
cwd: options.cwd,
|
|
15
|
+
env: options.env ? { ...options.env } : undefined,
|
|
16
|
+
stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
});
|
|
20
|
+
let stdout = Buffer.alloc(0);
|
|
21
|
+
let stderr = Buffer.alloc(0);
|
|
22
|
+
const append = (current, chunk) => {
|
|
23
|
+
if (current.length >= maxOutputBytes)
|
|
24
|
+
return current;
|
|
25
|
+
return Buffer.concat([current, chunk.subarray(0, maxOutputBytes - current.length)]);
|
|
26
|
+
};
|
|
27
|
+
child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
28
|
+
child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
|
|
29
|
+
child.on("error", (error) => {
|
|
30
|
+
cleanup();
|
|
31
|
+
reject(error);
|
|
32
|
+
});
|
|
33
|
+
child.on("close", (code) => {
|
|
34
|
+
cleanup();
|
|
35
|
+
resolve({ exitCode: code ?? 1, stdout: stdout.toString("utf8"), stderr: stderr.toString("utf8") });
|
|
36
|
+
});
|
|
37
|
+
if (options.stdin !== undefined) {
|
|
38
|
+
child.stdin.end(options.stdin);
|
|
39
|
+
}
|
|
40
|
+
function cleanup() {
|
|
41
|
+
if (timeout)
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
options.signal?.removeEventListener("abort", abort);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ValidatedSandboxContract } from "./sandbox.types.js";
|
|
2
|
+
export declare function validateSandboxContract(source: string): ValidatedSandboxContract;
|
|
3
|
+
export declare function validateDockerfilePolicy(source: string, requirePinnedBase: boolean): readonly string[];
|
|
4
|
+
export declare function dockerfileBaseImages(source: string): readonly string[];
|
|
5
|
+
export declare function dockerfileUpstreamImages(source: string): readonly string[];
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAlias, parseDocument, visit } from "yaml";
|
|
3
|
+
const maxContractBytes = 64 * 1024;
|
|
4
|
+
const maxDepth = 16;
|
|
5
|
+
const topLevelKeys = new Set(["schemaVersion", "workingDirectory", "commands", "caches", "services", "environment", "artifacts", "logs", "browser"]);
|
|
6
|
+
const forbiddenKeys = new Set([
|
|
7
|
+
"privileged", "cap_add", "capAdd", "devices", "device", "network_mode", "networkMode", "pid", "ipc",
|
|
8
|
+
"userns_mode", "usernsMode", "security_opt", "securityOpt", "volumes", "mounts", "binds", "dockerSocket",
|
|
9
|
+
"hostNetwork", "extra_hosts", "extraHosts", "ports", "secrets", "buildSecrets", "ssh", "entitlements"
|
|
10
|
+
]);
|
|
11
|
+
const allowedCaches = new Set(["npm", "pnpm", "yarn", "nuget", "pip", "cargo", "gradle", "maven"]);
|
|
12
|
+
const defaultPorts = { postgres: 5432, redis: 6379, browser: 9222 };
|
|
13
|
+
export function validateSandboxContract(source) {
|
|
14
|
+
const bytes = Buffer.byteLength(source, "utf8");
|
|
15
|
+
if (bytes === 0 || bytes > maxContractBytes)
|
|
16
|
+
throw new Error(`contract_invalid: forge.sandbox.yml must be 1-${maxContractBytes} bytes.`);
|
|
17
|
+
if (containsForbiddenControlCharacter(source)) {
|
|
18
|
+
throw new Error("contract_invalid: control characters are not allowed.");
|
|
19
|
+
}
|
|
20
|
+
const document = parseDocument(source, { uniqueKeys: true, prettyErrors: false, strict: true });
|
|
21
|
+
if (document.errors.length > 0)
|
|
22
|
+
throw new Error(`contract_invalid: ${document.errors[0]?.message ?? "invalid YAML"}`);
|
|
23
|
+
visit(document, {
|
|
24
|
+
Node(_key, node) {
|
|
25
|
+
if (isAlias(node) || node.anchor)
|
|
26
|
+
throw new Error("contract_invalid: YAML aliases and anchors are not allowed.");
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const parsed = document.toJS({ maxAliasCount: 0 });
|
|
30
|
+
assertDepth(parsed, 0);
|
|
31
|
+
const root = object(parsed, "contract");
|
|
32
|
+
rejectUnknownKeys(root, topLevelKeys, "contract");
|
|
33
|
+
rejectForbiddenKeys(root, "contract");
|
|
34
|
+
if (root.schemaVersion !== 1)
|
|
35
|
+
throw new Error("contract_invalid: schemaVersion must be 1.");
|
|
36
|
+
const workingDirectory = root.workingDirectory ?? "/workspace";
|
|
37
|
+
if (workingDirectory !== "/workspace")
|
|
38
|
+
throw new Error("contract_invalid: workingDirectory must be /workspace.");
|
|
39
|
+
const contract = {
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
workingDirectory: "/workspace",
|
|
42
|
+
commands: parseCommands(root.commands),
|
|
43
|
+
caches: parseCaches(root.caches),
|
|
44
|
+
services: parseServices(root.services),
|
|
45
|
+
environment: stringArray(root.environment, "environment", 64).map(validateEnvironmentName),
|
|
46
|
+
artifacts: stringArray(root.artifacts, "artifacts", 128).map((value) => validateRelativePath(value, "artifacts")),
|
|
47
|
+
logs: stringArray(root.logs, "logs", 128).map((value) => validateRelativePath(value, "logs")),
|
|
48
|
+
browser: optionalBoolean(root.browser, "browser") ?? false,
|
|
49
|
+
};
|
|
50
|
+
const normalizedJson = canonicalJson(contract);
|
|
51
|
+
const digest = `sha256:${createHash("sha256").update(normalizedJson).digest("hex")}`;
|
|
52
|
+
return { contract, normalizedJson, digest };
|
|
53
|
+
}
|
|
54
|
+
function containsForbiddenControlCharacter(value) {
|
|
55
|
+
return Array.from(value).some((character) => {
|
|
56
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
57
|
+
return codePoint === 0x7f
|
|
58
|
+
|| (codePoint < 0x20 && codePoint !== 0x09 && codePoint !== 0x0a && codePoint !== 0x0d);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function validateDockerfilePolicy(source, requirePinnedBase) {
|
|
62
|
+
if (Buffer.byteLength(source, "utf8") > 1024 * 1024)
|
|
63
|
+
throw new Error("contract_invalid: Dockerfile exceeds 1 MiB.");
|
|
64
|
+
if (containsForbiddenControlCharacter(source))
|
|
65
|
+
throw new Error("contract_invalid: Dockerfile control characters are not allowed.");
|
|
66
|
+
const logicalSource = source.replace(/\\\r?\n/gu, " ");
|
|
67
|
+
if (/^\s*#\s*syntax\s*=/imu.test(source)) {
|
|
68
|
+
throw new Error("contract_invalid: custom Dockerfile syntax frontends are forbidden.");
|
|
69
|
+
}
|
|
70
|
+
if (/^\s*#\s*escape\s*=/imu.test(source)) {
|
|
71
|
+
throw new Error("contract_invalid: custom Dockerfile escape directives are forbidden.");
|
|
72
|
+
}
|
|
73
|
+
if (/^\s*(?:ONBUILD\s+)?RUN\b[^\r\n]*--mount\s*=\s*type\s*=\s*(?:secret|ssh|bind)\b/imu.test(logicalSource)) {
|
|
74
|
+
throw new Error("contract_invalid: Dockerfile secret, SSH, and bind build mounts are forbidden.");
|
|
75
|
+
}
|
|
76
|
+
if (/^\s*(?:ONBUILD\s+)?RUN\b[^\r\n]*--(?:network\s*=\s*host|security\s*=\s*insecure|device(?:\s|=))/imu.test(logicalSource)) {
|
|
77
|
+
throw new Error("contract_invalid: Dockerfile host-network, insecure-security, and device build options are forbidden.");
|
|
78
|
+
}
|
|
79
|
+
if (/^\s*(?:ONBUILD\s+)?VOLUME(?:\s|\[)/imu.test(source)) {
|
|
80
|
+
throw new Error("contract_invalid: Dockerfile VOLUME instructions are forbidden; Forge owns writable mounts.");
|
|
81
|
+
}
|
|
82
|
+
const warnings = [];
|
|
83
|
+
for (const image of dockerfileUpstreamImages(source)) {
|
|
84
|
+
if (!/@sha256:[a-f0-9]{64}$/iu.test(image)) {
|
|
85
|
+
const message = `Upstream image '${image}' is not pinned by sha256 digest.`;
|
|
86
|
+
if (requirePinnedBase)
|
|
87
|
+
throw new Error(`contract_invalid: ${message}`);
|
|
88
|
+
warnings.push(message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return warnings;
|
|
92
|
+
}
|
|
93
|
+
export function dockerfileBaseImages(source) {
|
|
94
|
+
if (Buffer.byteLength(source, "utf8") > 1024 * 1024)
|
|
95
|
+
throw new Error("contract_invalid: Dockerfile exceeds 1 MiB.");
|
|
96
|
+
const fromLines = source.replace(/\\\r?\n/gu, " ").split(/\r?\n/u)
|
|
97
|
+
.map((line) => line.trim())
|
|
98
|
+
.filter((line) => /^FROM\s+/iu.test(line));
|
|
99
|
+
if (fromLines.length === 0)
|
|
100
|
+
throw new Error("contract_invalid: .forge/Dockerfile must contain FROM.");
|
|
101
|
+
const stageAliases = new Set();
|
|
102
|
+
const externalImages = [];
|
|
103
|
+
for (const line of fromLines) {
|
|
104
|
+
const tokens = line.split(/\s+/u).slice(1);
|
|
105
|
+
const imageIndex = tokens.findIndex((token) => !token.startsWith("--"));
|
|
106
|
+
const image = imageIndex >= 0 ? tokens[imageIndex] ?? "" : "";
|
|
107
|
+
if (!image || /[\r\n\0]/u.test(image))
|
|
108
|
+
throw new Error("contract_invalid: Dockerfile FROM image is invalid.");
|
|
109
|
+
if (!stageAliases.has(image.toLowerCase()))
|
|
110
|
+
externalImages.push(image);
|
|
111
|
+
const asIndex = tokens.findIndex((token, index) => index > imageIndex && token.toUpperCase() === "AS");
|
|
112
|
+
const alias = asIndex >= 0 ? tokens[asIndex + 1] : undefined;
|
|
113
|
+
if (alias) {
|
|
114
|
+
if (!/^[a-zA-Z0-9_.-]{1,128}$/u.test(alias))
|
|
115
|
+
throw new Error("contract_invalid: Dockerfile stage alias is invalid.");
|
|
116
|
+
stageAliases.add(alias.toLowerCase());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [...new Set(externalImages)];
|
|
120
|
+
}
|
|
121
|
+
export function dockerfileUpstreamImages(source) {
|
|
122
|
+
const bases = dockerfileBaseImages(source);
|
|
123
|
+
const logicalLines = source.replace(/\\\r?\n/gu, " ").split(/\r?\n/u).map(line => line.trim());
|
|
124
|
+
const stageAliases = new Set();
|
|
125
|
+
for (const line of logicalLines.filter(line => /^FROM\s+/iu.test(line))) {
|
|
126
|
+
const tokens = line.split(/\s+/u).slice(1);
|
|
127
|
+
const imageIndex = tokens.findIndex(token => !token.startsWith("--"));
|
|
128
|
+
const asIndex = tokens.findIndex((token, index) => index > imageIndex && token.toUpperCase() === "AS");
|
|
129
|
+
const alias = asIndex >= 0 ? tokens[asIndex + 1] : undefined;
|
|
130
|
+
if (alias)
|
|
131
|
+
stageAliases.add(alias.toLowerCase());
|
|
132
|
+
}
|
|
133
|
+
const copyImages = logicalLines
|
|
134
|
+
.filter(line => /^\s*(?:ONBUILD\s+)?COPY\b/iu.test(line))
|
|
135
|
+
.map(line => /(?:^|\s)--from=([^\s]+)/iu.exec(line)?.[1] ?? "")
|
|
136
|
+
.filter(image => image && !/^\d+$/u.test(image) && !stageAliases.has(image.toLowerCase()));
|
|
137
|
+
return [...new Set([...bases, ...copyImages])];
|
|
138
|
+
}
|
|
139
|
+
function parseCommands(value) {
|
|
140
|
+
if (value === undefined || value === null)
|
|
141
|
+
return {};
|
|
142
|
+
const commands = object(value, "commands");
|
|
143
|
+
if (Object.keys(commands).length > 32)
|
|
144
|
+
throw new Error("contract_invalid: commands cannot contain more than 32 entries.");
|
|
145
|
+
const result = {};
|
|
146
|
+
for (const [name, command] of Object.entries(commands)) {
|
|
147
|
+
if (!/^[a-z][a-z0-9_-]{0,63}$/u.test(name) || typeof command !== "string" || !command.trim() || command.length > 4096) {
|
|
148
|
+
throw new Error("contract_invalid: commands must map safe names to non-empty strings.");
|
|
149
|
+
}
|
|
150
|
+
result[name] = command.trim();
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
function parseCaches(value) {
|
|
155
|
+
if (value === undefined || value === null)
|
|
156
|
+
return [];
|
|
157
|
+
if (!Array.isArray(value))
|
|
158
|
+
throw new Error("contract_invalid: caches must be an array.");
|
|
159
|
+
if (value.length > allowedCaches.size)
|
|
160
|
+
throw new Error(`contract_invalid: caches cannot contain more than ${allowedCaches.size} entries.`);
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
return value.map((entry, index) => {
|
|
163
|
+
const cache = typeof entry === "string" ? { name: entry } : object(entry, `caches[${index}]`);
|
|
164
|
+
rejectUnknownKeys(cache, new Set(["name", "mode"]), `caches[${index}]`);
|
|
165
|
+
const name = requiredString(cache.name, `caches[${index}].name`).toLowerCase();
|
|
166
|
+
if (!allowedCaches.has(name))
|
|
167
|
+
throw new Error(`contract_invalid: cache '${name}' is not allowlisted.`);
|
|
168
|
+
if (seen.has(name))
|
|
169
|
+
throw new Error(`contract_invalid: duplicate cache '${name}'.`);
|
|
170
|
+
seen.add(name);
|
|
171
|
+
const mode = cache.mode ?? "readonly";
|
|
172
|
+
if (mode !== "readonly" && mode !== "per_attempt")
|
|
173
|
+
throw new Error("contract_invalid: cache mode must be readonly or per_attempt.");
|
|
174
|
+
return { name, mode };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function parseServices(value) {
|
|
178
|
+
if (value === undefined || value === null)
|
|
179
|
+
return [];
|
|
180
|
+
if (!Array.isArray(value))
|
|
181
|
+
throw new Error("contract_invalid: services must be an array.");
|
|
182
|
+
if (value.length > 3)
|
|
183
|
+
throw new Error("contract_invalid: services cannot contain more than 3 entries.");
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
return value.map((entry, index) => {
|
|
186
|
+
const service = typeof entry === "string" ? { type: entry } : object(entry, `services[${index}]`);
|
|
187
|
+
rejectUnknownKeys(service, new Set(["type", "port"]), `services[${index}]`);
|
|
188
|
+
const type = requiredString(service.type, `services[${index}].type`).toLowerCase();
|
|
189
|
+
if (type !== "postgres" && type !== "redis" && type !== "browser")
|
|
190
|
+
throw new Error(`contract_invalid: service '${type}' is not in the Forge catalogue.`);
|
|
191
|
+
if (seen.has(type))
|
|
192
|
+
throw new Error(`contract_invalid: duplicate service '${type}'.`);
|
|
193
|
+
seen.add(type);
|
|
194
|
+
const port = service.port === undefined ? defaultPorts[type] : positivePort(service.port, `services[${index}].port`);
|
|
195
|
+
return { type, port };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function rejectForbiddenKeys(value, path) {
|
|
199
|
+
if (Array.isArray(value))
|
|
200
|
+
return value.forEach((entry, index) => rejectForbiddenKeys(entry, `${path}[${index}]`));
|
|
201
|
+
if (!value || typeof value !== "object")
|
|
202
|
+
return;
|
|
203
|
+
for (const [key, child] of Object.entries(value)) {
|
|
204
|
+
if (forbiddenKeys.has(key))
|
|
205
|
+
throw new Error(`contract_invalid: forbidden privilege key '${path}.${key}'.`);
|
|
206
|
+
rejectForbiddenKeys(child, `${path}.${key}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function assertDepth(value, depth) {
|
|
210
|
+
if (depth > maxDepth)
|
|
211
|
+
throw new Error(`contract_invalid: YAML nesting exceeds ${maxDepth}.`);
|
|
212
|
+
if (Array.isArray(value)) {
|
|
213
|
+
value.forEach((entry) => assertDepth(entry, depth + 1));
|
|
214
|
+
}
|
|
215
|
+
else if (value && typeof value === "object") {
|
|
216
|
+
Object.values(value).forEach((entry) => assertDepth(entry, depth + 1));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function validateRelativePath(value, field) {
|
|
220
|
+
const normalized = value.trim().replace(/\\/gu, "/");
|
|
221
|
+
if (!normalized || normalized.length > 512 || normalized.startsWith("/") || normalized.includes("\0") || normalized.split("/").some((part) => part === ".." || part === "." || !part)) {
|
|
222
|
+
throw new Error(`contract_invalid: ${field} paths must be normalized relative paths under /workspace.`);
|
|
223
|
+
}
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
function validateEnvironmentName(value) {
|
|
227
|
+
const name = value.trim();
|
|
228
|
+
if (!/^[A-Z_][A-Z0-9_]{0,127}$/u.test(name) || name.startsWith("FORGE_") || name === "HOME" || name === "PATH") {
|
|
229
|
+
throw new Error(`contract_invalid: environment variable '${name}' is reserved or invalid.`);
|
|
230
|
+
}
|
|
231
|
+
return name;
|
|
232
|
+
}
|
|
233
|
+
function canonicalJson(value) {
|
|
234
|
+
if (Array.isArray(value))
|
|
235
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
236
|
+
if (value && typeof value === "object") {
|
|
237
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`).join(",")}}`;
|
|
238
|
+
}
|
|
239
|
+
return JSON.stringify(value);
|
|
240
|
+
}
|
|
241
|
+
function object(value, label) {
|
|
242
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
243
|
+
throw new Error(`contract_invalid: ${label} must be an object.`);
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
function rejectUnknownKeys(value, allowed, label) {
|
|
247
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
248
|
+
if (unknown.length > 0)
|
|
249
|
+
throw new Error(`contract_invalid: unknown ${label} key '${unknown[0]}'.`);
|
|
250
|
+
}
|
|
251
|
+
function stringArray(value, label, maximumCount) {
|
|
252
|
+
if (value === undefined || value === null)
|
|
253
|
+
return [];
|
|
254
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string"))
|
|
255
|
+
throw new Error(`contract_invalid: ${label} must be a string array.`);
|
|
256
|
+
if (value.length > maximumCount)
|
|
257
|
+
throw new Error(`contract_invalid: ${label} cannot contain more than ${maximumCount} entries.`);
|
|
258
|
+
return [...new Set(value.map((entry) => entry.trim()))];
|
|
259
|
+
}
|
|
260
|
+
function requiredString(value, label) {
|
|
261
|
+
if (typeof value !== "string" || !value.trim())
|
|
262
|
+
throw new Error(`contract_invalid: ${label} must be a non-empty string.`);
|
|
263
|
+
return value.trim();
|
|
264
|
+
}
|
|
265
|
+
function optionalBoolean(value, label) {
|
|
266
|
+
if (value === undefined || value === null)
|
|
267
|
+
return null;
|
|
268
|
+
if (typeof value !== "boolean")
|
|
269
|
+
throw new Error(`contract_invalid: ${label} must be boolean.`);
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
function positivePort(value, label) {
|
|
273
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 65535)
|
|
274
|
+
throw new Error(`contract_invalid: ${label} must be a valid internal port.`);
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class SandboxEventBudget {
|
|
2
|
+
private readonly maximumCount;
|
|
3
|
+
private readonly maximumBytes;
|
|
4
|
+
private readonly maximumPerSecond;
|
|
5
|
+
private totalCount;
|
|
6
|
+
private totalBytes;
|
|
7
|
+
private windowStartedMs;
|
|
8
|
+
private windowCount;
|
|
9
|
+
constructor(maximumCount?: number, maximumBytes?: number, maximumPerSecond?: number, nowMs?: number);
|
|
10
|
+
accept(line: string, nowMs?: number): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class SandboxEventBudget {
|
|
2
|
+
maximumCount;
|
|
3
|
+
maximumBytes;
|
|
4
|
+
maximumPerSecond;
|
|
5
|
+
totalCount = 0;
|
|
6
|
+
totalBytes = 0;
|
|
7
|
+
windowStartedMs;
|
|
8
|
+
windowCount = 0;
|
|
9
|
+
constructor(maximumCount = 100_000, maximumBytes = 64 * 1024 * 1024, maximumPerSecond = 1_000, nowMs = performance.now()) {
|
|
10
|
+
this.maximumCount = maximumCount;
|
|
11
|
+
this.maximumBytes = maximumBytes;
|
|
12
|
+
this.maximumPerSecond = maximumPerSecond;
|
|
13
|
+
this.windowStartedMs = nowMs;
|
|
14
|
+
}
|
|
15
|
+
accept(line, nowMs = performance.now()) {
|
|
16
|
+
if (nowMs - this.windowStartedMs >= 1_000) {
|
|
17
|
+
this.windowStartedMs = nowMs;
|
|
18
|
+
this.windowCount = 0;
|
|
19
|
+
}
|
|
20
|
+
this.totalCount += 1;
|
|
21
|
+
this.windowCount += 1;
|
|
22
|
+
this.totalBytes += Buffer.byteLength(line, "utf8");
|
|
23
|
+
if (this.totalCount > this.maximumCount || this.totalBytes > this.maximumBytes || this.windowCount > this.maximumPerSecond) {
|
|
24
|
+
throw new Error("sandbox_event_flood: sandbox event rate or total volume exceeded its bounded budget.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type SandboxProcessRequest, type SandboxProcessResult } from "./process.js";
|
|
2
|
+
export type SandboxImageLayer = "project" | "tooling" | "agent";
|
|
3
|
+
export interface SandboxImagePolicyRequest {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly project: string;
|
|
6
|
+
readonly executionPolicy: string;
|
|
7
|
+
readonly stateRoot: string;
|
|
8
|
+
readonly builderVersion: string;
|
|
9
|
+
readonly contextDigest: string;
|
|
10
|
+
readonly contractDigest: string;
|
|
11
|
+
readonly toolingImage: string;
|
|
12
|
+
readonly upstreamImages: readonly string[];
|
|
13
|
+
readonly codexVersion: string;
|
|
14
|
+
readonly images: Readonly<Record<SandboxImageLayer, string>>;
|
|
15
|
+
readonly imageScanTargets?: Readonly<Partial<Record<SandboxImageLayer, string>>>;
|
|
16
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
17
|
+
readonly signal?: AbortSignal;
|
|
18
|
+
readonly runCommand?: (command: SandboxProcessRequest) => Promise<SandboxProcessResult>;
|
|
19
|
+
}
|
|
20
|
+
export interface SandboxImagePolicyResult {
|
|
21
|
+
readonly status: "passed" | "warning";
|
|
22
|
+
readonly policyDigest: string;
|
|
23
|
+
readonly root: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function evaluateSandboxImagePolicy(request: SandboxImagePolicyRequest): Promise<SandboxImagePolicyResult>;
|
|
26
|
+
export interface SandboxImageSignatureRequest {
|
|
27
|
+
readonly image: string;
|
|
28
|
+
readonly executionPolicy: string;
|
|
29
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
30
|
+
readonly signal?: AbortSignal;
|
|
31
|
+
readonly runCommand?: (command: SandboxProcessRequest) => Promise<SandboxProcessResult>;
|
|
32
|
+
}
|
|
33
|
+
export declare function verifySandboxImageSignature(request: SandboxImageSignatureRequest): Promise<"passed" | "warning">;
|
|
34
|
+
export declare function hasSandboxImagePolicyRecord(stateRoot: string, key: string, expectedDigest: string, requirePassed?: boolean): Promise<boolean>;
|
|
35
|
+
export declare function sandboxImagePolicyConfigurationDigest(environment?: NodeJS.ProcessEnv): Promise<string>;
|