@workos/quickstudy 0.10.0 → 0.12.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.
@@ -0,0 +1,91 @@
1
+ import { hashString, SECRET_FILE } from "../hash.ts";
2
+ import { EXCLUDED_WORKSPACE_DIRS } from "../store/artifacts.ts";
3
+
4
+ /** Non-secret, regular, non-executable files. Source labels are declarations, not attestations. */
5
+ export interface WorkspaceFile {
6
+ path: string;
7
+ content: string;
8
+ encoding: "utf8" | "base64";
9
+ source: { id: string; version: string };
10
+ }
11
+
12
+ /** v1: create-only, no symlinks/hardlinks, 0644 files, final read-back, no deletion. */
13
+ export interface WorkspaceFiles {
14
+ version: 1;
15
+ files: readonly WorkspaceFile[];
16
+ absent: readonly string[];
17
+ }
18
+
19
+ export interface WorkspaceFilesIdentity {
20
+ version: 1;
21
+ files: readonly { path: string; size: number; sha256: string; source: { id: string; version: string } }[];
22
+ absent: readonly string[];
23
+ }
24
+
25
+ export const WORKSPACE_FILES_LIMITS = Object.freeze({ files: 128, absent: 128, fileBytes: 262144, totalBytes: 1048576, pathBytes: 240, depth: 16, timeoutMs: 30000 });
26
+
27
+ function invalid(reason: string): never {
28
+ // Never interpolate consumer data: even malformed metadata may contain secrets.
29
+ throw new Error(`workspaceFiles: ${reason}`);
30
+ }
31
+
32
+ function pathCheck(path: unknown): asserts path is string {
33
+ if (typeof path !== "string" || path.length > WORKSPACE_FILES_LIMITS.pathBytes) invalid("invalid relative path");
34
+ const parts = path.split("/");
35
+ if (parts.length > WORKSPACE_FILES_LIMITS.depth || parts.some((part) =>
36
+ !/^[A-Za-z0-9_.-]+$/.test(part) || part === "." || part === ".." || part.endsWith(".") ||
37
+ /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\.|$)/i.test(part) ||
38
+ EXCLUDED_WORKSPACE_DIRS.includes(part.toLowerCase()) || SECRET_FILE.test(part) ||
39
+ /^\.(?:git|hg|svn)/i.test(part) || /^\.quickstudy/i.test(part)
40
+ )) invalid("unsafe or reserved relative path");
41
+ }
42
+
43
+ export function pathsOverlap(a: string, b: string): boolean {
44
+ a = a.toLowerCase(); b = b.toLowerCase();
45
+ return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
46
+ }
47
+
48
+ /** Validate, copy, canonicalize to base64, and freeze without retaining mutable caller state. */
49
+ export function defineWorkspaceFiles(input: WorkspaceFiles): WorkspaceFiles {
50
+ if (!input || input.version !== 1) invalid("unsupported staging version");
51
+ if (!Array.isArray(input.files) || !Array.isArray(input.absent) ||
52
+ input.files.length > WORKSPACE_FILES_LIMITS.files || input.absent.length > WORKSPACE_FILES_LIMITS.absent) invalid("invalid inventory or count limit exceeded");
53
+ let total = 0;
54
+ const files = input.files.map((file) => {
55
+ if (!file || typeof file.content !== "string" || file.content.length > WORKSPACE_FILES_LIMITS.fileBytes * 4) invalid("invalid or oversized payload");
56
+ pathCheck(file.path);
57
+ if (!file.source || ![file.source.id, file.source.version].every((v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:@+-]{0,159}$/.test(v))) invalid("invalid source id/version (use opaque labels, not paths or URLs)");
58
+ if (file.encoding !== "utf8" && file.encoding !== "base64") invalid("unsupported encoding");
59
+ const bytes = Buffer.from(file.content, file.encoding === "utf8" ? "utf8" : "base64");
60
+ if (file.encoding === "base64" && bytes.toString("base64") !== file.content) invalid("base64 must be canonical padded RFC 4648");
61
+ if (file.encoding === "utf8" && bytes.toString("utf8") !== file.content) invalid("utf8 must not contain unpaired surrogates");
62
+ total += bytes.length;
63
+ if (bytes.length > WORKSPACE_FILES_LIMITS.fileBytes || total > WORKSPACE_FILES_LIMITS.totalBytes) invalid("payload size limit exceeded");
64
+ return Object.freeze({ path: file.path, content: bytes.toString("base64"), encoding: "base64" as const, source: Object.freeze({ id: file.source.id, version: file.source.version }) });
65
+ }).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
66
+ const absent = [...input.absent];
67
+ for (const path of absent) pathCheck(path);
68
+ const paths = [...files.map((file) => file.path), ...absent];
69
+ for (let i = 0; i < paths.length; i++) for (let j = i + 1; j < paths.length; j++) {
70
+ if (pathsOverlap(paths[i]!, paths[j]!)) invalid("duplicate or overlapping destinations/absence assertions");
71
+ }
72
+ return Object.freeze({ version: 1, files: Object.freeze(files), absent: Object.freeze(absent.sort()) });
73
+ }
74
+
75
+ export function workspaceFilesIdentity(input: WorkspaceFiles): WorkspaceFilesIdentity {
76
+ const declaration = defineWorkspaceFiles(input);
77
+ return {
78
+ version: declaration.version,
79
+ files: declaration.files.map((file) => {
80
+ const bytes = Buffer.from(file.content, "base64");
81
+ return { path: file.path, size: bytes.length, sha256: hashString(bytes), source: file.source };
82
+ }),
83
+ absent: declaration.absent,
84
+ };
85
+ }
86
+
87
+ export function validateWorkspaceFilesRuntime(runtime: { kind: string; command?: string[]; workspaceFiles?: WorkspaceFiles }): void {
88
+ if (runtime.workspaceFiles === undefined) return;
89
+ defineWorkspaceFiles(runtime.workspaceFiles);
90
+ if (runtime.kind !== "container" || (runtime.command?.length ?? 0) > 0) invalid("requires a container adapter; host and scripted commands are unsupported");
91
+ }
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { cp, mkdir, writeFile, lstat, realpath, readlink } from "node:fs/promises";
24
24
  import { basename, join, resolve } from "node:path";
25
+ import type { WorkspaceFilesEvidence } from "../isolation/workspace-files.ts";
25
26
  import type { TranscriptEvent } from "../adapters/types.ts";
26
27
 
27
28
  /** Dependency directories never copied into an exported workspace. */
@@ -73,6 +74,12 @@ export class ArtifactsStore {
73
74
  return paths;
74
75
  }
75
76
 
77
+ /** Placement evidence, distinct from intended identity; proves no model use. */
78
+ async writeWorkspaceFilesEvidence(runId: string, attemptId: string, evidence: WorkspaceFilesEvidence): Promise<void> {
79
+ const paths = await this.prepareAttempt(runId, attemptId);
80
+ await writeFile(join(paths.attemptDir, "workspace-files.json"), `${JSON.stringify(evidence, null, 2)}\n`, "utf8");
81
+ }
82
+
76
83
  async writeTranscript(runId: string, attemptId: string, events: TranscriptEvent[]): Promise<string> {
77
84
  const paths = await this.prepareAttempt(runId, attemptId);
78
85
  const jsonl = events.map((event) => JSON.stringify(event)).join("\n");