mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
fix(cloud): sandbox-control typecheck to zero (SANDBOX-CONTROL-TYPECHECK)
This commit is contained in:
@@ -189,7 +189,7 @@ describe('Worker-embedded image scripts', () => {
|
||||
|
||||
it('installs every VM script and makes zsh read the session env hook once', () => {
|
||||
const provision = readFileSync(fileURLToPath(new URL('./provision-golden.sh', import.meta.url).href), 'utf8')
|
||||
const loop = provision.match(/^for name in (.+); do$/m)?.[1].split(' ') ?? []
|
||||
const loop = provision.match(/^for name in (.+); do$/m)?.[1]?.split(' ') ?? []
|
||||
expect(new Set(loop)).toEqual(new Set(VM_SCRIPTS))
|
||||
expect(provision).toContain(
|
||||
"grep -q zz-orca-session-env /etc/zsh/zshenv ||\n echo '[ -r /etc/profile.d/zz-orca-session-env.sh ] && . /etc/profile.d/zz-orca-session-env.sh' | sudo tee -a /etc/zsh/zshenv"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev": "wrangler dev -c wrangler.jsonc -c wrangler.build-runner.jsonc --port 8791 --ip 127.0.0.1",
|
||||
"db:migrate:local": "wrangler d1 migrations apply SANDBOX_DB --local",
|
||||
"golden-image": "tsx image/build-golden-image.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// SANDBOX-CONTROL-TYPECHECK: `pnpm typecheck` for this Worker. tsc checks every file the program reaches, and
|
||||
// the Worker imports desktop code from src/shared (wire types, parsers). Those files are written for the
|
||||
// desktop's compiler options (no noUncheckedIndexedAccess) and globals (window, MessageChannel), and the
|
||||
// desktop's own typecheck covers them; TypeScript has no per-file options. So this fails on any diagnostic in
|
||||
// this package's own files, or one tsc reports without a file (a config error), and counts the rest.
|
||||
// node scripts/typecheck.mjs
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
/** Splits tsc --pretty false output into this package's diagnostics (with their detail lines) and a count of others. */
|
||||
export function splitDiagnostics(output) {
|
||||
const own = []
|
||||
let foreign = 0
|
||||
let keep = false
|
||||
for (const line of output.split('\n')) {
|
||||
const located = /^(.+?)\(\d+,\d+\): error TS\d+/.exec(line)
|
||||
if (located) {
|
||||
// Paths are relative to this package: anything reached through `..` is not its own.
|
||||
keep = !located[1].startsWith('..') && !located[1].startsWith('/')
|
||||
if (keep) {
|
||||
own.push(line)
|
||||
} else {
|
||||
foreign++
|
||||
}
|
||||
} else if (/^error TS\d+/.test(line)) {
|
||||
keep = true
|
||||
own.push(line)
|
||||
} else if (keep && line.startsWith(' ') && line.trim()) {
|
||||
own.push(line)
|
||||
} else if (line.trim()) {
|
||||
keep = false
|
||||
}
|
||||
}
|
||||
return { own, foreign }
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const tsc = createRequire(import.meta.url).resolve('typescript/bin/tsc')
|
||||
const run = spawnSync(process.execPath, [tsc, '--noEmit', '-p', '.', '--pretty', 'false'], { encoding: 'utf8' })
|
||||
const { own, foreign } = splitDiagnostics(`${run.stdout ?? ''}${run.stderr ?? ''}`)
|
||||
if (own.length > 0) {
|
||||
process.stdout.write(`${own.join('\n')}\n`)
|
||||
}
|
||||
if (foreign > 0) {
|
||||
process.stdout.write(`(${foreign} diagnostics in imported desktop files: checked by the desktop typecheck, not here)\n`)
|
||||
}
|
||||
const ownErrors = own.filter((line) => / error TS\d+/.test(line) || /^error TS\d+/.test(line)).length
|
||||
process.stdout.write(`sandbox-control typecheck: ${ownErrors} error(s) in its own files\n`)
|
||||
// A crash, or a non-zero exit with nothing parsed, is never a pass.
|
||||
process.exit(ownErrors > 0 || run.status === null || (run.status !== 0 && foreign === 0) ? 1 : 0)
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export function providerEnabled(env: Pick<Env, 'AGENT_ACCOUNTS_CLAUDE' | 'AGENT_
|
||||
return { claude: states.claude === 'on', codex: states.codex === 'on' }
|
||||
}
|
||||
|
||||
export function credentialState(row: Pick<CredentialRow, 'state' | 'expires_at'> | null, now: number): CredentialState {
|
||||
export function credentialState(row: Pick<CredentialRow, 'state' | 'expires_at' | 'suspect_at'> | null, now: number): CredentialState {
|
||||
if (!row) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ export type SandboxStatus =
|
||||
|
||||
export type SandboxRow = {
|
||||
id: string
|
||||
/** What made a machine-made workspace (a routine run: its routine's id); null for desktop creates. */
|
||||
origin_ref?: string | null
|
||||
account_id: string
|
||||
user_id: string
|
||||
workspace_id: string
|
||||
|
||||
@@ -63,6 +63,6 @@ export async function tarGzipFile(input: { dirs: string[]; path: string; bytes:
|
||||
new Uint8Array(padding),
|
||||
new Uint8Array(BLOCK * 2) // end-of-archive marker
|
||||
]
|
||||
const stream = new Blob(parts as BlobPart[]).stream().pipeThrough(new CompressionStream('gzip'))
|
||||
const stream = new Blob(parts).stream().pipeThrough(new CompressionStream('gzip'))
|
||||
return new Response(stream).arrayBuffer()
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ export async function listWorkspaces(db: D1Database, caller: SandboxCaller, url:
|
||||
|
||||
/** sandbox id -> its routine's name, for routine runs being listed. */
|
||||
async function routineNamesFor(db: D1Database, accountId: string, rows: readonly SandboxRow[]): Promise<Map<string, string>> {
|
||||
const refs = [...new Set(rows.flatMap((row) => ((row as { origin_ref?: string | null }).origin_ref ? [(row as { origin_ref: string }).origin_ref] : [])))]
|
||||
const refs = [...new Set(rows.flatMap((row) => (row.origin_ref ? [row.origin_ref] : [])))]
|
||||
if (refs.length === 0) {
|
||||
return new Map()
|
||||
}
|
||||
@@ -318,7 +318,7 @@ async function routineNamesFor(db: D1Database, accountId: string, rows: readonly
|
||||
const byRoutine = new Map(results.map((r) => [r.id, r.name]))
|
||||
const out = new Map<string, string>()
|
||||
for (const row of rows) {
|
||||
const name = byRoutine.get((row as { origin_ref?: string | null }).origin_ref ?? '')
|
||||
const name = byRoutine.get(row.origin_ref ?? '')
|
||||
if (name) {
|
||||
out.set(row.id, name)
|
||||
}
|
||||
@@ -438,8 +438,8 @@ export async function patchWorkspace(
|
||||
}
|
||||
const ownerChanged = transferring
|
||||
const wentPrivate = row.visibility === 'team' && next.visibility === 'private'
|
||||
const archived = patch.archived === true && row.archived_at === null
|
||||
return { row: next, revoke: ownerChanged ? 'all_but_new_owner' : wentPrivate || archived ? 'non_owner' : 'none' }
|
||||
// Archiving goes through the lifecycle (archive_via_lifecycle above); the team route revokes on archive itself.
|
||||
return { row: next, revoke: ownerChanged ? 'all_but_new_owner' : wentPrivate ? 'non_owner' : 'none' }
|
||||
}
|
||||
|
||||
export const PresenceSchema = z
|
||||
|
||||
@@ -214,7 +214,7 @@ describe('public API: attachments', () => {
|
||||
const header = Buffer.from(tar.subarray(block * 512, block * 512 + 512))
|
||||
const stored = parseInt(field(block, 148, 8), 8)
|
||||
header.fill(0x20, 148, 156)
|
||||
expect(header.reduce((sum, b) => sum + b, 0)).toBe(stored)
|
||||
expect(header.reduce((sum: number, b: number) => sum + b, 0)).toBe(stored)
|
||||
}
|
||||
expect(tar.subarray(3 * 512, 3 * 512 + 5).toString()).toBe('hello')
|
||||
await expect(tarGzipFile({ dirs: [], path: '../x', bytes, mtime: 0 })).rejects.toThrow('tar_bad_path')
|
||||
|
||||
@@ -67,7 +67,10 @@ export type FakeVercel = SandboxProviderApi & GoldenImageProviderApi & {
|
||||
snapshots: Map<string, ProviderSnapshot & { sandboxName: string; creationMethod: string; createdAt: number }>
|
||||
commands: { sessionId: string; command: string; args?: string[]; env: Record<string, string> }[]
|
||||
/** Answers commands the fake does not know (in-VM orca CLI, routine runners); undefined falls through. */
|
||||
commandHandler?: (params: RunCommandParams, sessionId: string) => { exitCode: number; stdout: string; stderr: string } | undefined
|
||||
commandHandler?: (
|
||||
params: RunCommandParams,
|
||||
sessionId: string
|
||||
) => { exitCode: number; stdout: string; stderr: string } | Promise<{ exitCode: number; stdout: string; stderr: string }> | undefined
|
||||
failStart: boolean
|
||||
failEnvRefresh: boolean
|
||||
/** Exit code orca-ssh-keys answers with (0 = success; 3 = image without sshd). */
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('SEC-6c: secret-handling code logs only through the redacting sink', ()
|
||||
|
||||
it('the secret-handling modules never call console directly', () => {
|
||||
for (const file of FILES) {
|
||||
const source = readFileSync(fileURLToPath(new URL(`../src/${file}`, import.meta.url)), 'utf8')
|
||||
const source = readFileSync(fileURLToPath(new URL(`../src/${file}`, import.meta.url).href), 'utf8')
|
||||
expect({ file, direct: /\bconsole\s*\.\s*[a-z]+\s*\(/.test(source) }).toEqual({ file, direct: false })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// SANDBOX-CONTROL-TYPECHECK: the package typecheck fails on this package's own diagnostics (and config errors),
|
||||
// and only counts those in imported desktop files.
|
||||
import { describe, expect, it } from 'vitest'
|
||||
// @ts-expect-error -- plain .mjs script; no type declarations
|
||||
import { splitDiagnostics } from '../scripts/typecheck.mjs'
|
||||
|
||||
describe('scripts/typecheck.mjs', () => {
|
||||
it('keeps own diagnostics with their detail lines, counts imported desktop ones', () => {
|
||||
const out = [
|
||||
"src/a.ts(1,2): error TS2322: Type 'x' is not assignable to type 'y'.",
|
||||
" Type 'x' is missing properties.",
|
||||
"../../../src/shared/b.ts(3,4): error TS2532: Object is possibly 'undefined'.",
|
||||
" detail of a foreign error",
|
||||
"test/c.test.ts(5,6): error TS7006: Parameter 'p' implicitly has an 'any' type.",
|
||||
''
|
||||
].join('\n')
|
||||
const { own, foreign } = splitDiagnostics(out)
|
||||
expect(own).toEqual([
|
||||
"src/a.ts(1,2): error TS2322: Type 'x' is not assignable to type 'y'.",
|
||||
" Type 'x' is missing properties.",
|
||||
"test/c.test.ts(5,6): error TS7006: Parameter 'p' implicitly has an 'any' type."
|
||||
])
|
||||
expect(foreign).toBe(1)
|
||||
})
|
||||
|
||||
it('a diagnostic without a file (config error) is its own', () => {
|
||||
expect(splitDiagnostics("error TS2688: Cannot find type definition file for 'x'.").own).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -3,12 +3,26 @@
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["es2022"],
|
||||
"types": ["@cloudflare/workers-types", "@types/node"],
|
||||
"lib": [
|
||||
"es2024"
|
||||
],
|
||||
"types": [
|
||||
"@cloudflare/workers-types",
|
||||
"@types/node"
|
||||
],
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@xterm/xterm": [
|
||||
"./types/xterm-theme.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src", "test", "image"]
|
||||
"include": [
|
||||
"src",
|
||||
"test",
|
||||
"image"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// SANDBOX-CONTROL-TYPECHECK: desktop shared settings types reach `import type { ITheme } from '@xterm/xterm'`, and
|
||||
// xterm's typings pull in `/// <reference lib="dom" />`. DOM's Headers, streams and crypto then clash with
|
||||
// @cloudflare/workers-types across this Worker. tsconfig `paths` maps the import here: the theme shape only (copied
|
||||
// from @xterm/xterm's ITheme), no DOM.
|
||||
export interface ITheme {
|
||||
foreground?: string
|
||||
background?: string
|
||||
cursor?: string
|
||||
cursorAccent?: string
|
||||
selectionBackground?: string
|
||||
selectionForeground?: string
|
||||
selectionInactiveBackground?: string
|
||||
scrollbarSliderBackground?: string
|
||||
scrollbarSliderHoverBackground?: string
|
||||
scrollbarSliderActiveBackground?: string
|
||||
overviewRulerBorder?: string
|
||||
black?: string
|
||||
red?: string
|
||||
green?: string
|
||||
yellow?: string
|
||||
blue?: string
|
||||
magenta?: string
|
||||
cyan?: string
|
||||
white?: string
|
||||
brightBlack?: string
|
||||
brightRed?: string
|
||||
brightGreen?: string
|
||||
brightYellow?: string
|
||||
brightBlue?: string
|
||||
brightMagenta?: string
|
||||
brightCyan?: string
|
||||
brightWhite?: string
|
||||
extendedAnsi?: string[]
|
||||
}
|
||||
Reference in New Issue
Block a user