open-managed-agents 0.1.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/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createReadStream, chmodSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { invalidRequest } from "../errors.js";
|
|
6
|
+
import { newFileId } from "../ids.js";
|
|
7
|
+
import { withSqliteTransaction } from "../sqlite-transaction.js";
|
|
8
|
+
import { MAX_SESSION_OUTPUT_BYTES, MAX_SESSION_OUTPUT_FILENAME_BYTES, MAX_SESSION_OUTPUT_FILE_BYTES, MAX_SESSION_OUTPUT_FILES, MAX_UPLOADED_FILE_BYTES, MAX_WORKSPACE_FILE_BYTES, } from "./types.js";
|
|
9
|
+
import { compareRecords, consumeUploadBody, limitLabel, normalizeLimit, reusableOutput, toRecord, validateOutputFilename, } from "./store-common.js";
|
|
10
|
+
const FILE_STORAGE_SCHEMA = `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
workspace_id TEXT NOT NULL,
|
|
14
|
+
visibility TEXT NOT NULL,
|
|
15
|
+
kind TEXT NOT NULL,
|
|
16
|
+
scope_id TEXT,
|
|
17
|
+
relative_path TEXT,
|
|
18
|
+
filename TEXT NOT NULL,
|
|
19
|
+
mime_type TEXT NOT NULL,
|
|
20
|
+
size_bytes INTEGER NOT NULL,
|
|
21
|
+
created_at TEXT NOT NULL,
|
|
22
|
+
downloadable INTEGER NOT NULL,
|
|
23
|
+
scope TEXT NOT NULL,
|
|
24
|
+
storage_key TEXT NOT NULL UNIQUE,
|
|
25
|
+
sha256 TEXT NOT NULL
|
|
26
|
+
);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS files_by_workspace_public
|
|
28
|
+
ON files (workspace_id, visibility, scope_id, id);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS files_session_outputs
|
|
30
|
+
ON files (workspace_id, kind, scope_id, id);
|
|
31
|
+
`;
|
|
32
|
+
export class LocalObjectFileStorage {
|
|
33
|
+
db;
|
|
34
|
+
objectRoot;
|
|
35
|
+
objectsDir;
|
|
36
|
+
tmpDir;
|
|
37
|
+
insertStmt;
|
|
38
|
+
retrievePublicStmt;
|
|
39
|
+
retrieveInternalStmt;
|
|
40
|
+
retrieveStoredStmt;
|
|
41
|
+
deletePublicStmt;
|
|
42
|
+
deleteInternalStmt;
|
|
43
|
+
deleteSessionOutputsStmt;
|
|
44
|
+
listPublicStmt;
|
|
45
|
+
listPublicAfterStmt;
|
|
46
|
+
listPublicBeforeStmt;
|
|
47
|
+
listSessionOutputsStmt;
|
|
48
|
+
listSessionOutputsAfterStmt;
|
|
49
|
+
listSessionOutputsBeforeStmt;
|
|
50
|
+
sessionOutputsStmt;
|
|
51
|
+
workspaceBytesStmt;
|
|
52
|
+
maxUploadedFileBytes;
|
|
53
|
+
maxWorkspaceFileBytes;
|
|
54
|
+
maxSessionOutputFiles;
|
|
55
|
+
maxSessionOutputFileBytes;
|
|
56
|
+
maxSessionOutputBytes;
|
|
57
|
+
maxSessionOutputFilenameBytes;
|
|
58
|
+
constructor(db, objectRoot, opts = {}) {
|
|
59
|
+
this.db = db;
|
|
60
|
+
this.objectRoot = resolve(objectRoot);
|
|
61
|
+
this.objectsDir = resolve(this.objectRoot, "objects");
|
|
62
|
+
this.tmpDir = resolve(this.objectRoot, "tmp");
|
|
63
|
+
mkdirSync(this.objectRoot, { recursive: true, mode: 0o700 });
|
|
64
|
+
mkdirSync(this.objectsDir, { recursive: true, mode: 0o700 });
|
|
65
|
+
mkdirSync(this.tmpDir, { recursive: true, mode: 0o700 });
|
|
66
|
+
chmodSync(this.objectRoot, 0o700);
|
|
67
|
+
chmodSync(this.objectsDir, 0o700);
|
|
68
|
+
chmodSync(this.tmpDir, 0o700);
|
|
69
|
+
this.db.exec(FILE_STORAGE_SCHEMA);
|
|
70
|
+
this.insertStmt = this.db.prepare(`INSERT INTO files (
|
|
71
|
+
id, workspace_id, visibility, kind, scope_id, relative_path, filename,
|
|
72
|
+
mime_type, size_bytes, created_at, downloadable, scope, storage_key,
|
|
73
|
+
sha256
|
|
74
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
75
|
+
this.retrievePublicStmt = this.db.prepare(`SELECT * FROM files
|
|
76
|
+
WHERE workspace_id = ? AND id = ? AND visibility = 'public'`);
|
|
77
|
+
this.retrieveInternalStmt = this.db.prepare(`SELECT * FROM files
|
|
78
|
+
WHERE workspace_id = ? AND id = ? AND visibility = 'internal'`);
|
|
79
|
+
this.retrieveStoredStmt = this.db.prepare(`SELECT * FROM files WHERE workspace_id = ? AND id = ?`);
|
|
80
|
+
this.deletePublicStmt = this.db.prepare(`DELETE FROM files
|
|
81
|
+
WHERE workspace_id = ? AND id = ? AND visibility = 'public'`);
|
|
82
|
+
this.deleteInternalStmt = this.db.prepare(`DELETE FROM files
|
|
83
|
+
WHERE workspace_id = ? AND id = ? AND visibility = 'internal'`);
|
|
84
|
+
this.deleteSessionOutputsStmt = this.db.prepare(`DELETE FROM files
|
|
85
|
+
WHERE workspace_id = ? AND kind = 'session_output' AND scope_id = ?`);
|
|
86
|
+
this.listPublicStmt = this.db.prepare(publicListSql("", "ASC"));
|
|
87
|
+
this.listPublicAfterStmt = this.db.prepare(publicListSql("AND id > ?", "ASC"));
|
|
88
|
+
this.listPublicBeforeStmt = this.db.prepare(publicListSql("AND id < ?", "DESC"));
|
|
89
|
+
this.listSessionOutputsStmt = this.db.prepare(sessionOutputListSql("", "ASC"));
|
|
90
|
+
this.listSessionOutputsAfterStmt = this.db.prepare(sessionOutputListSql("AND id > ?", "ASC"));
|
|
91
|
+
this.listSessionOutputsBeforeStmt = this.db.prepare(sessionOutputListSql("AND id < ?", "DESC"));
|
|
92
|
+
this.sessionOutputsStmt = this.db.prepare(`SELECT * FROM files
|
|
93
|
+
WHERE workspace_id = ? AND visibility = 'public'
|
|
94
|
+
AND kind = 'session_output' AND scope_id = ?
|
|
95
|
+
ORDER BY id ASC`);
|
|
96
|
+
this.workspaceBytesStmt = this.db.prepare(`SELECT COALESCE(SUM(size_bytes), 0) AS total
|
|
97
|
+
FROM files WHERE workspace_id = ?`);
|
|
98
|
+
this.maxUploadedFileBytes =
|
|
99
|
+
opts.maxUploadedFileBytes ?? MAX_UPLOADED_FILE_BYTES;
|
|
100
|
+
this.maxWorkspaceFileBytes =
|
|
101
|
+
opts.maxWorkspaceFileBytes ?? MAX_WORKSPACE_FILE_BYTES;
|
|
102
|
+
this.maxSessionOutputFiles =
|
|
103
|
+
opts.maxSessionOutputFiles ?? MAX_SESSION_OUTPUT_FILES;
|
|
104
|
+
this.maxSessionOutputFileBytes =
|
|
105
|
+
opts.maxSessionOutputFileBytes ?? MAX_SESSION_OUTPUT_FILE_BYTES;
|
|
106
|
+
this.maxSessionOutputBytes =
|
|
107
|
+
opts.maxSessionOutputBytes ?? MAX_SESSION_OUTPUT_BYTES;
|
|
108
|
+
this.maxSessionOutputFilenameBytes =
|
|
109
|
+
opts.maxSessionOutputFilenameBytes ?? MAX_SESSION_OUTPUT_FILENAME_BYTES;
|
|
110
|
+
this.sweepTempAndOrphanedObjects();
|
|
111
|
+
}
|
|
112
|
+
async create(workspaceId, input) {
|
|
113
|
+
return this.createStored(workspaceId, input, {
|
|
114
|
+
visibility: "public",
|
|
115
|
+
kind: "upload",
|
|
116
|
+
scope: null,
|
|
117
|
+
scopeId: null,
|
|
118
|
+
fileId: undefined,
|
|
119
|
+
downloadable: false,
|
|
120
|
+
relativePath: undefined,
|
|
121
|
+
maxBytes: this.maxUploadedFileBytes,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async createInternalSnapshot(workspaceId, input) {
|
|
125
|
+
return this.createStored(workspaceId, input, {
|
|
126
|
+
visibility: "internal",
|
|
127
|
+
kind: "internal_snapshot",
|
|
128
|
+
scope: { type: "session", id: input.scopeId },
|
|
129
|
+
scopeId: input.scopeId,
|
|
130
|
+
fileId: input.fileId,
|
|
131
|
+
downloadable: false,
|
|
132
|
+
relativePath: undefined,
|
|
133
|
+
maxBytes: this.maxUploadedFileBytes,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async createStored(workspaceId, input, opts) {
|
|
137
|
+
const { bytes, sizeBytes, sha256 } = await consumeUploadBody(input.body, opts.maxBytes, opts.kind === "session_output" ? "Session output file" : "Uploaded file");
|
|
138
|
+
const id = opts.fileId ?? newFileId();
|
|
139
|
+
const now = new Date().toISOString();
|
|
140
|
+
let objectPath;
|
|
141
|
+
try {
|
|
142
|
+
return this.withTransaction(() => {
|
|
143
|
+
if (this.workspaceBytes(workspaceId) + sizeBytes >
|
|
144
|
+
this.maxWorkspaceFileBytes) {
|
|
145
|
+
throw invalidRequest(`Workspace file storage exceeds the ${limitLabel(this.maxWorkspaceFileBytes)} local-object limit`);
|
|
146
|
+
}
|
|
147
|
+
if (this.retrieveStored(workspaceId, id)) {
|
|
148
|
+
throw invalidRequest(`File ${id} already exists`);
|
|
149
|
+
}
|
|
150
|
+
const object = this.writeObjectSync(bytes);
|
|
151
|
+
objectPath = object.path;
|
|
152
|
+
const stored = {
|
|
153
|
+
visibility: opts.visibility,
|
|
154
|
+
kind: opts.kind,
|
|
155
|
+
workspace_id: workspaceId,
|
|
156
|
+
storage_key: object.storageKey,
|
|
157
|
+
sha256,
|
|
158
|
+
bytes,
|
|
159
|
+
scope_id: opts.scopeId,
|
|
160
|
+
relative_path: opts.relativePath,
|
|
161
|
+
metadata: {
|
|
162
|
+
id,
|
|
163
|
+
type: "file",
|
|
164
|
+
filename: input.filename,
|
|
165
|
+
mime_type: input.mimeType,
|
|
166
|
+
size_bytes: sizeBytes,
|
|
167
|
+
created_at: now,
|
|
168
|
+
downloadable: opts.downloadable,
|
|
169
|
+
scope: opts.scope,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
this.insertStored(stored);
|
|
173
|
+
return toRecord(stored);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (objectPath !== undefined)
|
|
178
|
+
await unlinkIfExists(objectPath);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async retrieveMetadata(workspaceId, fileId) {
|
|
183
|
+
const row = this.retrievePublicStmt.get(workspaceId, fileId);
|
|
184
|
+
return row ? toRecord(rowToStored(row)) : undefined;
|
|
185
|
+
}
|
|
186
|
+
async openBytes(workspaceId, fileId) {
|
|
187
|
+
const row = this.retrievePublicStmt.get(workspaceId, fileId);
|
|
188
|
+
if (!row)
|
|
189
|
+
return undefined;
|
|
190
|
+
const path = this.objectPath(row.storage_key);
|
|
191
|
+
if (!existsSync(path))
|
|
192
|
+
return undefined;
|
|
193
|
+
return createReadStream(path);
|
|
194
|
+
}
|
|
195
|
+
async openInternalSnapshotBytes(workspaceId, fileId) {
|
|
196
|
+
const row = this.retrieveInternalStmt.get(workspaceId, fileId);
|
|
197
|
+
if (!row)
|
|
198
|
+
return undefined;
|
|
199
|
+
const path = this.objectPath(row.storage_key);
|
|
200
|
+
if (!existsSync(path))
|
|
201
|
+
return undefined;
|
|
202
|
+
return createReadStream(path);
|
|
203
|
+
}
|
|
204
|
+
async delete(workspaceId, fileId) {
|
|
205
|
+
const deleted = this.deletePublicRow(workspaceId, fileId);
|
|
206
|
+
if (!deleted)
|
|
207
|
+
return false;
|
|
208
|
+
await unlinkIfExists(this.objectPath(deleted.storage_key));
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
async deleteInternalSnapshot(workspaceId, fileId) {
|
|
212
|
+
const deleted = this.deleteInternalRow(workspaceId, fileId);
|
|
213
|
+
if (!deleted)
|
|
214
|
+
return false;
|
|
215
|
+
await unlinkIfExists(this.objectPath(deleted.storage_key));
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
async replaceSessionOutputs(workspaceId, sessionId, files) {
|
|
219
|
+
return this.replaceSessionOutputsIfLive(workspaceId, sessionId, files, () => true);
|
|
220
|
+
}
|
|
221
|
+
async replaceSessionOutputsIfLive(workspaceId, sessionId, files, canCommit) {
|
|
222
|
+
if (files.length > this.maxSessionOutputFiles) {
|
|
223
|
+
throw invalidRequest(`Session output collection exceeds the ${this.maxSessionOutputFiles} file limit`);
|
|
224
|
+
}
|
|
225
|
+
const basenameOwners = new Map();
|
|
226
|
+
const preparedInputs = [];
|
|
227
|
+
let prepared = [];
|
|
228
|
+
let oldOutputs = [];
|
|
229
|
+
const newObjectPaths = [];
|
|
230
|
+
let committed = false;
|
|
231
|
+
let totalOutputBytes = 0;
|
|
232
|
+
try {
|
|
233
|
+
for (const file of files) {
|
|
234
|
+
validateOutputFilename(file.filename, this.maxSessionOutputFilenameBytes);
|
|
235
|
+
const existingRelativePath = basenameOwners.get(file.filename);
|
|
236
|
+
if (existingRelativePath !== undefined &&
|
|
237
|
+
existingRelativePath !== file.relativePath) {
|
|
238
|
+
throw invalidRequest(`Session output filename collision for '${file.filename}' from '${existingRelativePath}' and '${file.relativePath}'`);
|
|
239
|
+
}
|
|
240
|
+
basenameOwners.set(file.filename, file.relativePath);
|
|
241
|
+
if (file.sizeBytes !== undefined &&
|
|
242
|
+
file.sizeBytes > this.maxSessionOutputFileBytes) {
|
|
243
|
+
throw invalidRequest(`Session output file exceeds the ${limitLabel(this.maxSessionOutputFileBytes)} per-file limit`);
|
|
244
|
+
}
|
|
245
|
+
if (file.sizeBytes !== undefined &&
|
|
246
|
+
totalOutputBytes + file.sizeBytes > this.maxSessionOutputBytes) {
|
|
247
|
+
throw invalidRequest(`Session outputs exceed the ${limitLabel(this.maxSessionOutputBytes)} aggregate limit`);
|
|
248
|
+
}
|
|
249
|
+
const { bytes, sizeBytes, sha256 } = await consumeUploadBody(file.body, this.maxSessionOutputFileBytes, "Session output file");
|
|
250
|
+
if (file.sizeBytes !== undefined && file.sizeBytes !== sizeBytes) {
|
|
251
|
+
throw invalidRequest(`Session output '${file.relativePath}' size changed during collection`);
|
|
252
|
+
}
|
|
253
|
+
if (file.sha256 !== undefined && file.sha256 !== sha256) {
|
|
254
|
+
throw invalidRequest(`Session output '${file.relativePath}' checksum changed during collection`);
|
|
255
|
+
}
|
|
256
|
+
totalOutputBytes += sizeBytes;
|
|
257
|
+
if (totalOutputBytes > this.maxSessionOutputBytes) {
|
|
258
|
+
throw invalidRequest(`Session outputs exceed the ${limitLabel(this.maxSessionOutputBytes)} aggregate limit`);
|
|
259
|
+
}
|
|
260
|
+
preparedInputs.push({ input: file, bytes, sizeBytes, sha256 });
|
|
261
|
+
}
|
|
262
|
+
this.withTransaction(() => {
|
|
263
|
+
if (!canCommit()) {
|
|
264
|
+
throw invalidRequest(`Session outputs cannot be committed for inactive session ${sessionId}`);
|
|
265
|
+
}
|
|
266
|
+
oldOutputs = this.sessionOutputFiles(workspaceId, sessionId);
|
|
267
|
+
const reusableOutputByRelativePath = new Map(oldOutputs
|
|
268
|
+
.filter((stored) => stored.relative_path !== undefined)
|
|
269
|
+
.map((stored) => [stored.relative_path, stored]));
|
|
270
|
+
prepared = preparedInputs.map(({ input, bytes, sizeBytes, sha256 }) => {
|
|
271
|
+
const reusable = reusableOutput(reusableOutputByRelativePath.get(input.relativePath), input, sizeBytes, sha256);
|
|
272
|
+
const id = reusable?.metadata.id ?? newFileId();
|
|
273
|
+
const createdAt = reusable?.metadata.created_at ?? new Date().toISOString();
|
|
274
|
+
return {
|
|
275
|
+
visibility: "public",
|
|
276
|
+
kind: "session_output",
|
|
277
|
+
workspace_id: workspaceId,
|
|
278
|
+
storage_key: reusable?.storage_key ?? "",
|
|
279
|
+
sha256,
|
|
280
|
+
bytes,
|
|
281
|
+
scope_id: sessionId,
|
|
282
|
+
relative_path: input.relativePath,
|
|
283
|
+
metadata: {
|
|
284
|
+
id,
|
|
285
|
+
type: "file",
|
|
286
|
+
filename: input.filename,
|
|
287
|
+
mime_type: input.mimeType,
|
|
288
|
+
size_bytes: sizeBytes,
|
|
289
|
+
created_at: createdAt,
|
|
290
|
+
downloadable: true,
|
|
291
|
+
scope: { type: "session", id: sessionId },
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
const oldBytes = oldOutputs.reduce((sum, stored) => sum + stored.metadata.size_bytes, 0);
|
|
296
|
+
if (this.workspaceBytes(workspaceId) - oldBytes + totalOutputBytes >
|
|
297
|
+
this.maxWorkspaceFileBytes) {
|
|
298
|
+
throw invalidRequest(`Workspace file storage exceeds the ${limitLabel(this.maxWorkspaceFileBytes)} local-object limit`);
|
|
299
|
+
}
|
|
300
|
+
for (const stored of prepared) {
|
|
301
|
+
if (stored.storage_key !== "")
|
|
302
|
+
continue;
|
|
303
|
+
const object = this.writeObjectSync(stored.bytes);
|
|
304
|
+
stored.storage_key = object.storageKey;
|
|
305
|
+
newObjectPaths.push(object.path);
|
|
306
|
+
}
|
|
307
|
+
this.deleteSessionOutputsStmt.run(workspaceId, sessionId);
|
|
308
|
+
for (const stored of prepared) {
|
|
309
|
+
this.insertStored(stored);
|
|
310
|
+
}
|
|
311
|
+
committed = true;
|
|
312
|
+
});
|
|
313
|
+
const keptStorageKeys = new Set(prepared.map((stored) => stored.storage_key));
|
|
314
|
+
await Promise.all(oldOutputs
|
|
315
|
+
.filter((stored) => !keptStorageKeys.has(stored.storage_key))
|
|
316
|
+
.map((stored) => unlinkIfExists(this.objectPath(stored.storage_key))));
|
|
317
|
+
return prepared.map(toRecord).sort(compareRecords);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
if (!committed) {
|
|
321
|
+
await Promise.all(newObjectPaths.map(unlinkIfExists));
|
|
322
|
+
}
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
deleteSessionOutputRows(workspaceId, sessionId) {
|
|
327
|
+
return this.withTransaction(() => {
|
|
328
|
+
const oldOutputs = this.sessionOutputFiles(workspaceId, sessionId);
|
|
329
|
+
this.deleteSessionOutputsStmt.run(workspaceId, sessionId);
|
|
330
|
+
return oldOutputs.map(toRecord).sort(compareRecords);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async deleteObjectsForRecords(records) {
|
|
334
|
+
await Promise.all(records.map((record) => unlinkIfExists(this.objectPath(record.storage_key))));
|
|
335
|
+
}
|
|
336
|
+
async deleteSessionOutputs(workspaceId, sessionId) {
|
|
337
|
+
const oldOutputs = this.deleteSessionOutputRows(workspaceId, sessionId);
|
|
338
|
+
await this.deleteObjectsForRecords(oldOutputs);
|
|
339
|
+
}
|
|
340
|
+
async list(workspaceId, opts = {}) {
|
|
341
|
+
const limit = normalizeLimit(opts.limit);
|
|
342
|
+
const queryLimit = limit + 1;
|
|
343
|
+
const rows = this.selectListRows(workspaceId, opts, queryLimit)
|
|
344
|
+
.map(rowToStored)
|
|
345
|
+
.map(toRecord);
|
|
346
|
+
const pageRows = rows.slice(0, limit);
|
|
347
|
+
const data = opts.beforeId === undefined
|
|
348
|
+
? pageRows
|
|
349
|
+
: pageRows.sort(compareRecords);
|
|
350
|
+
return {
|
|
351
|
+
data,
|
|
352
|
+
has_more: rows.length > limit,
|
|
353
|
+
first_id: data[0]?.metadata.id ?? null,
|
|
354
|
+
last_id: data[data.length - 1]?.metadata.id ?? null,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
selectListRows(workspaceId, opts, limit) {
|
|
358
|
+
if (opts.scopeId === undefined) {
|
|
359
|
+
if (opts.beforeId !== undefined) {
|
|
360
|
+
return this.listPublicBeforeStmt.all(workspaceId, opts.beforeId, limit);
|
|
361
|
+
}
|
|
362
|
+
if (opts.afterId !== undefined) {
|
|
363
|
+
return this.listPublicAfterStmt.all(workspaceId, opts.afterId, limit);
|
|
364
|
+
}
|
|
365
|
+
return this.listPublicStmt.all(workspaceId, limit);
|
|
366
|
+
}
|
|
367
|
+
if (opts.beforeId !== undefined) {
|
|
368
|
+
return this.listSessionOutputsBeforeStmt.all(workspaceId, opts.scopeId, opts.beforeId, limit);
|
|
369
|
+
}
|
|
370
|
+
if (opts.afterId !== undefined) {
|
|
371
|
+
return this.listSessionOutputsAfterStmt.all(workspaceId, opts.scopeId, opts.afterId, limit);
|
|
372
|
+
}
|
|
373
|
+
return this.listSessionOutputsStmt.all(workspaceId, opts.scopeId, limit);
|
|
374
|
+
}
|
|
375
|
+
retrieveStored(workspaceId, fileId) {
|
|
376
|
+
const row = this.retrieveStoredStmt.get(workspaceId, fileId);
|
|
377
|
+
return row ? rowToStored(row) : undefined;
|
|
378
|
+
}
|
|
379
|
+
insertStored(stored) {
|
|
380
|
+
this.insertStmt.run(stored.metadata.id, stored.workspace_id, stored.visibility, stored.kind, stored.scope_id, stored.relative_path ?? null, stored.metadata.filename, stored.metadata.mime_type, stored.metadata.size_bytes, stored.metadata.created_at, stored.metadata.downloadable ? 1 : 0, JSON.stringify(stored.metadata.scope), stored.storage_key, stored.sha256);
|
|
381
|
+
}
|
|
382
|
+
sessionOutputFiles(workspaceId, sessionId) {
|
|
383
|
+
return this.sessionOutputsStmt.all(workspaceId, sessionId).map(rowToStored);
|
|
384
|
+
}
|
|
385
|
+
workspaceBytes(workspaceId) {
|
|
386
|
+
const row = this.workspaceBytesStmt.get(workspaceId);
|
|
387
|
+
return row?.total ?? 0;
|
|
388
|
+
}
|
|
389
|
+
deletePublicRow(workspaceId, fileId) {
|
|
390
|
+
return this.withTransaction(() => {
|
|
391
|
+
const row = this.retrievePublicStmt.get(workspaceId, fileId);
|
|
392
|
+
if (!row)
|
|
393
|
+
return undefined;
|
|
394
|
+
this.deletePublicStmt.run(workspaceId, fileId);
|
|
395
|
+
return row;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
deleteInternalRow(workspaceId, fileId) {
|
|
399
|
+
return this.withTransaction(() => {
|
|
400
|
+
const row = this.retrieveInternalStmt.get(workspaceId, fileId);
|
|
401
|
+
if (!row)
|
|
402
|
+
return undefined;
|
|
403
|
+
this.deleteInternalStmt.run(workspaceId, fileId);
|
|
404
|
+
return row;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
writeObjectSync(bytes) {
|
|
408
|
+
const storageKey = `${randomUUID()}.bin`;
|
|
409
|
+
const tmpPath = resolve(this.tmpDir, `${storageKey}.tmp`);
|
|
410
|
+
const finalPath = this.objectPath(storageKey);
|
|
411
|
+
writeFileSync(tmpPath, bytes, { mode: 0o600 });
|
|
412
|
+
renameSync(tmpPath, finalPath);
|
|
413
|
+
return { storageKey, path: finalPath };
|
|
414
|
+
}
|
|
415
|
+
withTransaction(fn) {
|
|
416
|
+
return withSqliteTransaction(this.db, fn);
|
|
417
|
+
}
|
|
418
|
+
objectPath(storageKey) {
|
|
419
|
+
const path = resolve(this.objectsDir, storageKey);
|
|
420
|
+
if (!path.startsWith(`${this.objectsDir}/`)) {
|
|
421
|
+
throw new Error(`Invalid file storage key: ${storageKey}`);
|
|
422
|
+
}
|
|
423
|
+
return path;
|
|
424
|
+
}
|
|
425
|
+
sweepTempAndOrphanedObjects() {
|
|
426
|
+
rmSync(this.tmpDir, { recursive: true, force: true });
|
|
427
|
+
mkdirSync(this.tmpDir, { recursive: true, mode: 0o700 });
|
|
428
|
+
const liveKeys = new Set(this.db.prepare("SELECT storage_key FROM files").all().map((row) => row.storage_key));
|
|
429
|
+
for (const entry of readdirSync(this.objectsDir, { withFileTypes: true })) {
|
|
430
|
+
if (!entry.isFile())
|
|
431
|
+
continue;
|
|
432
|
+
if (!liveKeys.has(entry.name)) {
|
|
433
|
+
unlinkSync(resolve(this.objectsDir, entry.name));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function publicListSql(cursorPredicate, order) {
|
|
439
|
+
return `
|
|
440
|
+
SELECT * FROM files
|
|
441
|
+
WHERE workspace_id = ? AND visibility = 'public' AND scope_id IS NULL
|
|
442
|
+
${cursorPredicate}
|
|
443
|
+
ORDER BY id ${order}
|
|
444
|
+
LIMIT ?
|
|
445
|
+
`;
|
|
446
|
+
}
|
|
447
|
+
function sessionOutputListSql(cursorPredicate, order) {
|
|
448
|
+
return `
|
|
449
|
+
SELECT * FROM files
|
|
450
|
+
WHERE workspace_id = ? AND visibility = 'public'
|
|
451
|
+
AND kind = 'session_output' AND scope_id = ?
|
|
452
|
+
${cursorPredicate}
|
|
453
|
+
ORDER BY id ${order}
|
|
454
|
+
LIMIT ?
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
function rowToStored(row) {
|
|
458
|
+
return {
|
|
459
|
+
workspace_id: row.workspace_id,
|
|
460
|
+
visibility: row.visibility,
|
|
461
|
+
kind: row.kind,
|
|
462
|
+
scope_id: row.scope_id,
|
|
463
|
+
relative_path: row.relative_path ?? undefined,
|
|
464
|
+
storage_key: row.storage_key,
|
|
465
|
+
sha256: row.sha256,
|
|
466
|
+
bytes: new Uint8Array(),
|
|
467
|
+
metadata: {
|
|
468
|
+
id: row.id,
|
|
469
|
+
type: "file",
|
|
470
|
+
filename: row.filename,
|
|
471
|
+
mime_type: row.mime_type,
|
|
472
|
+
size_bytes: row.size_bytes,
|
|
473
|
+
created_at: row.created_at,
|
|
474
|
+
downloadable: row.downloadable === 1,
|
|
475
|
+
scope: JSON.parse(row.scope),
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async function unlinkIfExists(path) {
|
|
480
|
+
try {
|
|
481
|
+
await unlink(path);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (error.code !== "ENOENT") {
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|