mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 02:57:36 +00:00
* feat(orcad): ship the mobile web page so phones paired to orcad get it orcad never carried out/mobile-web, so it did not advertise mobileWeb.bundle.v1 and phones paired directly to an orcad server fell back to the app's built-in native screens. The built page now ships in every orcad package at out/mobile-web, the path getBundledMobileWebBundleRoot already probes under the install root. Its manifest is a fixed artifact; its content-addressed assets join the hashed artifact list from that manifest, as one shared copy in the template's common files. build-orcad fails when the page is missing or stale, and CI lanes that build orcad now build the page first. * ci(orcad): check out mobile/ for the Windows host template build * refactor(orcad): copy the page tree whole and build it once per CI run The guard already refuses unlisted files, so staging copies the bundle directory as native-chat-visuals is copied. The shipped template takes the page the qualification lanes tested, the e2e build artifact carries it, and the PR smoke builds it through install-mobile-dependencies behind its own scope step. Drops tests that duplicated the guard's own. * fix(orcad): refuse a template page asset name that escapes the slot
295 lines
13 KiB
TypeScript
295 lines
13 KiB
TypeScript
/**
|
|
* What a packaged `orcad` directory must contain, declared once: the build, the content hash
|
|
* and the remote install probe must not keep three lists that drift.
|
|
*
|
|
* Order is load-bearing: the hash concatenates these files in sequence.
|
|
*
|
|
* Keep this file erasable-only TypeScript — build-orcad.mjs imports it directly under
|
|
* Node's type stripping, which rejects enums, namespaces and parameter properties.
|
|
*/
|
|
// Bun-era slots only: kept so a client can still launch one for rollback (design D7.1 R5).
|
|
export const ORCAD_BUN_RUNTIME_FILENAME = 'bun-runtime'
|
|
export const ORCAD_WINDOWS_BUN_RUNTIME_FILENAME = 'bun-runtime.exe'
|
|
export const ORCAD_BUILD_TARGET_FILENAME = '.build-target'
|
|
export const ORCAD_WINDOWS_PROCESS_TREE_FILENAME = 'windows-process-tree.node'
|
|
/** native/windows-security: secure-file ACLs without spawning icacls. */
|
|
export const ORCAD_WINDOWS_SECURITY_FILENAME = 'windows-security.node'
|
|
/** native/windows-cli-launcher: cmd.exe and .ps1 shims cannot carry every argv intact. */
|
|
export const ORCAD_WINDOWS_CLI_LAUNCHER_FILENAME = 'bin/orca.exe'
|
|
|
|
export function orcadBunRuntimeFilename(target: string): string {
|
|
return isWindowsTarget(target) ? ORCAD_WINDOWS_BUN_RUNTIME_FILENAME : ORCAD_BUN_RUNTIME_FILENAME
|
|
}
|
|
|
|
function isWindowsTarget(target: string): boolean {
|
|
return target === 'win32' || target.startsWith('win32-')
|
|
}
|
|
|
|
/** A Node slot's server target (e.g. linux-x64-musl); `.build-target` would mark it as Bun. */
|
|
export const ORCAD_SERVER_TARGET_FILENAME = '.server-target'
|
|
|
|
/**
|
|
* Marks a slot launched by the pinned Node runtime; its content is that runtime's
|
|
* executableSha256 (node-runtime-pin.ts), which is how the runtime enters the version hash.
|
|
* A Node slot must never carry `.build-target`: Bun-era selectors exit 78 on `.build-target`
|
|
* without `bun-runtime` (design D7.1 R5).
|
|
*/
|
|
export const ORCAD_NODE_RUNTIME_MARKER_FILENAME = '.runtime-node'
|
|
/** Beside the slot dirs and shared across Orca versions (design D2): `runtimes/node-<sha256>/bin/node`. */
|
|
export const ORCAD_RUNTIMES_DIRNAME = 'runtimes'
|
|
export const ORCAD_NODE_RUNTIME_DIR_PREFIX = 'node-'
|
|
export const ORCAD_NODE_RUNTIME_POSIX_EXECUTABLE = 'bin/node'
|
|
// Upstream's own name and layout: renaming node.exe is the masquerading pattern EDR scores.
|
|
export const ORCAD_NODE_RUNTIME_WINDOWS_EXECUTABLE = 'node.exe'
|
|
|
|
export function orcadNodeRuntimeExecutable(target: string): string {
|
|
return isWindowsTarget(target)
|
|
? ORCAD_NODE_RUNTIME_WINDOWS_EXECUTABLE
|
|
: ORCAD_NODE_RUNTIME_POSIX_EXECUTABLE
|
|
}
|
|
|
|
/** Slot-relative path segments of the runtime a slot's marker names. */
|
|
export function orcadNodeRuntimeRelativePath(target: string, executableSha256: string): string[] {
|
|
return [
|
|
'..',
|
|
ORCAD_RUNTIMES_DIRNAME,
|
|
`${ORCAD_NODE_RUNTIME_DIR_PREFIX}${executableSha256}`,
|
|
...orcadNodeRuntimeExecutable(target).split('/')
|
|
]
|
|
}
|
|
|
|
/** N-API level the slot addons are built for; equals SLOT_NAPI_VERSION in the prebuild script. */
|
|
export const ORCAD_ADDON_NAPI_VERSION = 8
|
|
|
|
export const ORCAD_NODE_PTY_DIR = 'node_modules/node-pty'
|
|
export const ORCAD_CLI_ENTRY_FILENAME = 'out/cli/index.js'
|
|
export const ORCAD_CLI_PACKAGE_FILENAME = 'out/package.json'
|
|
// Test files and sources stay out; these are every module the runtime path requires.
|
|
export const ORCAD_NODE_PTY_JS_ARTIFACTS = [
|
|
'package.json',
|
|
'lib/conpty_console_list_agent.js',
|
|
'lib/eventEmitter2.js',
|
|
'lib/index.js',
|
|
'lib/interfaces.js',
|
|
'lib/shared/conout.js',
|
|
'lib/terminal.js',
|
|
'lib/types.js',
|
|
'lib/unixTerminal.js',
|
|
'lib/utils.js',
|
|
'lib/windowsConoutConnection.js',
|
|
'lib/windowsPtyAgent.js',
|
|
'lib/windowsTerminal.js',
|
|
'lib/worker/conoutSocketWorker.js'
|
|
].map((file) => `${ORCAD_NODE_PTY_DIR}/${file}`)
|
|
|
|
/** The prebuild slot's files (config/scripts/orcad-prebuild-slot-contents.mjs), relative to build/Release. */
|
|
export function orcadNodePtySlotFiles(target: string): string[] {
|
|
if (isWindowsTarget(target)) {
|
|
return [
|
|
'conpty.node',
|
|
'conpty_console_list.node',
|
|
'conpty/conpty.dll',
|
|
'conpty/OpenConsole.exe'
|
|
]
|
|
}
|
|
return target.startsWith('darwin-') ? ['pty.node', 'spawn-helper'] : ['pty.node']
|
|
}
|
|
|
|
export function orcadNodePtyNativeArtifacts(target: string): string[] {
|
|
return orcadNodePtySlotFiles(target).map((file) => `${ORCAD_NODE_PTY_DIR}/build/Release/${file}`)
|
|
}
|
|
|
|
export const ORCAD_PARCEL_WATCHER_ENTRY = 'node_modules/@parcel/watcher/index.js'
|
|
export const ORCAD_PARCEL_WATCHER_NATIVE = 'node_modules/@parcel/watcher/watcher.node'
|
|
export const ORCAD_EMOJI_SHORTCODE_DATASET =
|
|
'node_modules/emojibase-data/en/shortcodes/emojibase.json'
|
|
// linear-sdk.ts loads it through createRequire, which the server bundle cannot follow.
|
|
export const ORCAD_LINEAR_SDK_ENTRY = 'node_modules/@linear/sdk/index.js'
|
|
|
|
export const ORCAD_VERSION = '0.1.0'
|
|
export const ORCAD_LAUNCHER_FILENAME = 'orcad.js'
|
|
export const ORCAD_SERVER_ENTRY_FILENAME = 'orcad-server.js'
|
|
|
|
// Equals FOREIGN_SQLITE_READER_ENTRY_FILENAME; that module is not loadable under type stripping.
|
|
export const ORCAD_FOREIGN_SQLITE_READER_ENTRY = 'foreign-sqlite-reader-entry.js'
|
|
/** Worker thread that runs workspace port detection's probe commands off the event loop. */
|
|
export const ORCAD_PORT_SCAN_COMMAND_WORKER_ENTRY = 'port-scan-command-worker-entry.js'
|
|
// Equals AI_VAULT_SERVICE_ENTRY_FILENAME: the forked child that lists agent sessions.
|
|
export const ORCAD_SESSION_SCANNER_SERVICE_ENTRY = 'session-scanner-service-entry.js'
|
|
|
|
// Kept here because build-orcad.mjs imports this manifest directly under Node type stripping.
|
|
export const ORCAD_RIPGREP_ARTIFACTS = [
|
|
'ripgrep/linux-x64/rg',
|
|
'ripgrep/linux-arm64/rg',
|
|
'ripgrep/darwin-x64/rg',
|
|
'ripgrep/darwin-arm64/rg',
|
|
'ripgrep/win32-x64/rg.exe',
|
|
'ripgrep/win32-arm64/rg.exe'
|
|
] as const
|
|
|
|
/** Only the target's own ripgrep ships (design D2); both libcs share one static build. */
|
|
export function orcadRipgrepArtifact(target: string): string {
|
|
const [platform, arch] = target.split('-')
|
|
const artifact = ORCAD_RIPGREP_ARTIFACTS.find((candidate) =>
|
|
candidate.startsWith(`ripgrep/${platform}-${arch}/`)
|
|
)
|
|
if (!artifact) {
|
|
throw new Error(`orcad ships no ripgrep for ${target}`)
|
|
}
|
|
return artifact
|
|
}
|
|
|
|
export const ORCAD_RIPGREP_LICENSE_ARTIFACTS = [
|
|
'ripgrep/licenses/JEMALLOC-COPYING',
|
|
'ripgrep/licenses/LICENSE-MIT',
|
|
'ripgrep/licenses/LLVM-LIBUNWIND-LICENSE.TXT',
|
|
'ripgrep/licenses/MUSL-COPYRIGHT',
|
|
'ripgrep/licenses/PCRE2-LICENCE.md',
|
|
'ripgrep/licenses/README.md',
|
|
'ripgrep/licenses/RUST-CRATE-NOTICES.txt',
|
|
'ripgrep/licenses/SLJIT-LICENSE',
|
|
'ripgrep/licenses/UNLICENSE'
|
|
] as const
|
|
|
|
/** The skill plugin native-chat agents load by path; mirrors resources/native-chat-visuals. */
|
|
export const ORCAD_NATIVE_CHAT_VISUALS_ARTIFACTS = [
|
|
'native-chat-visuals/.claude-plugin/plugin.json',
|
|
'native-chat-visuals/skills/orca-chat-visuals/SKILL.md'
|
|
] as const
|
|
|
|
/**
|
|
* The page phones load over mobileWeb.bundle.*, at the path getBundledMobileWebBundleRoot probes
|
|
* under the install root. Its assets are content-addressed, so only its manifest names them.
|
|
*/
|
|
export const ORCAD_MOBILE_WEB_DIR = 'out/mobile-web'
|
|
export const ORCAD_MOBILE_WEB_MANIFEST = `${ORCAD_MOBILE_WEB_DIR}/manifest.json`
|
|
|
|
// Mirrors manifest-contract ASSET_PATH_PATTERN; cannot import under type stripping.
|
|
const MOBILE_WEB_ASSET_PATH = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/
|
|
|
|
/** A bundle-relative asset path that stays inside the page directory. */
|
|
export function isOrcadMobileWebAssetPath(path: string): boolean {
|
|
return (
|
|
MOBILE_WEB_ASSET_PATH.test(path) &&
|
|
!path.split('/').some((segment) => segment === '.' || segment === '..')
|
|
)
|
|
}
|
|
|
|
/** Slot paths of every asset `manifestText` lists, sorted; throws rather than ship a partial page. */
|
|
export function orcadMobileWebAssetFilenames(manifestText: string): string[] {
|
|
const manifest: unknown = JSON.parse(manifestText)
|
|
const assets =
|
|
manifest && typeof manifest === 'object' && 'assets' in manifest ? manifest.assets : undefined
|
|
if (!Array.isArray(assets) || assets.length === 0) {
|
|
throw new Error(`${ORCAD_MOBILE_WEB_MANIFEST} lists no assets`)
|
|
}
|
|
const filenames = assets.map((asset: unknown) => {
|
|
const path = asset && typeof asset === 'object' && 'path' in asset ? asset.path : undefined
|
|
if (typeof path !== 'string' || !isOrcadMobileWebAssetPath(path)) {
|
|
throw new Error(`${ORCAD_MOBILE_WEB_MANIFEST} names an unsafe asset path: ${String(path)}`)
|
|
}
|
|
return `${ORCAD_MOBILE_WEB_DIR}/${path}`
|
|
})
|
|
return filenames.sort(compareCodeUnits)
|
|
}
|
|
|
|
/** Code-unit order: page asset lists feed the install hash, so they must not depend on locale. */
|
|
export function compareCodeUnits(left: string, right: string): number {
|
|
return left < right ? -1 : left > right ? 1 : 0
|
|
}
|
|
|
|
export type OrcadArtifact = {
|
|
filename: string
|
|
/**
|
|
* Absence is a degradation, not a torn install, so the remote probe must not require it.
|
|
* The agent-browser binary is the only one: `resolveOrcadBrowserProvider` already answers
|
|
* "no headless browser" when it is missing, and it is named per platform-arch anyway.
|
|
*/
|
|
optional?: boolean
|
|
}
|
|
|
|
export const ORCAD_ARTIFACTS: readonly OrcadArtifact[] = [
|
|
{ filename: ORCAD_CLI_ENTRY_FILENAME },
|
|
{ filename: ORCAD_CLI_PACKAGE_FILENAME },
|
|
{ filename: ORCAD_LAUNCHER_FILENAME },
|
|
{ filename: ORCAD_SERVER_ENTRY_FILENAME },
|
|
// Forked so a native @parcel/watcher fault kills the child, not the server.
|
|
{ filename: 'parcel-watcher-process-entry.js' },
|
|
// Forked so PTYs outlive the runtime process; its absence makes every restart destructive.
|
|
{ filename: 'daemon-entry.js' },
|
|
{ filename: 'profile-state-writer-worker-entry.js' },
|
|
{ filename: 'profile-state-backup-worker-entry.js' },
|
|
// Worker thread that reads other apps' SQLite (the OpenCode binder and history) off the event loop.
|
|
{ filename: ORCAD_FOREIGN_SQLITE_READER_ENTRY },
|
|
{ filename: ORCAD_PORT_SCAN_COMMAND_WORKER_ENTRY },
|
|
// Forked so transcript parsing stays off the server's event loop; session search stays in-process.
|
|
{ filename: ORCAD_SESSION_SCANNER_SERVICE_ENTRY },
|
|
// Worker threads the account services start: CLI probes off the event loop, Claude account setup.
|
|
{ filename: 'worker-thread-child-process-entry.js' },
|
|
{ filename: 'claude-profile-setup-worker-entry.js' },
|
|
// Target-specific even when the JavaScript bundle is shared across packaged slots.
|
|
{ filename: ORCAD_SERVER_TARGET_FILENAME },
|
|
// orcad never depends on a host runtime or host-installed native module.
|
|
{ filename: ORCAD_NODE_RUNTIME_MARKER_FILENAME },
|
|
{ filename: ORCAD_PARCEL_WATCHER_ENTRY },
|
|
{ filename: ORCAD_PARCEL_WATCHER_NATIVE },
|
|
{ filename: ORCAD_EMOJI_SHORTCODE_DATASET },
|
|
{ filename: ORCAD_LINEAR_SDK_ENTRY },
|
|
...ORCAD_NODE_PTY_JS_ARTIFACTS.map((filename) => ({ filename })),
|
|
...ORCAD_RIPGREP_LICENSE_ARTIFACTS.map((filename) => ({ filename })),
|
|
...ORCAD_NATIVE_CHAT_VISUALS_ARTIFACTS.map((filename) => ({ filename })),
|
|
// Without it phones paired to this server get no page (mobileWeb.bundle.v1 is not advertised).
|
|
{ filename: ORCAD_MOBILE_WEB_MANIFEST }
|
|
]
|
|
|
|
/** Written after the artifacts, so it is never an input to its own hash. */
|
|
export const ORCAD_VERSION_FILENAME = '.version'
|
|
export const ORCAD_TEMPLATE_MANIFEST_FILENAME = 'orcad-template.json'
|
|
export const ORCAD_TEMPLATE_TARGETS_DIR = 'targets'
|
|
|
|
/** Written last by the installer; its absence means a torn install. */
|
|
export const ORCAD_INSTALL_COMPLETE_FILENAME = '.install-complete'
|
|
|
|
/**
|
|
* Every file a `target` slot ships, in hash order; `target` is `<os>-<arch>[-<libc>]`.
|
|
* `mobileWebAssets` is orcadMobileWebAssetFilenames of the slot's own page manifest.
|
|
*/
|
|
export function orcadArtifactFilenames(
|
|
target: string,
|
|
mobileWebAssets: readonly string[]
|
|
): string[] {
|
|
const filenames = ORCAD_ARTIFACTS.filter((artifact) => !artifact.optional).map(
|
|
(artifact) => artifact.filename
|
|
)
|
|
filenames.push(...orcadNodePtyNativeArtifacts(target), orcadRipgrepArtifact(target))
|
|
if (isWindowsTarget(target)) {
|
|
filenames.push(
|
|
ORCAD_WINDOWS_PROCESS_TREE_FILENAME,
|
|
ORCAD_WINDOWS_SECURITY_FILENAME,
|
|
ORCAD_WINDOWS_CLI_LAUNCHER_FILENAME
|
|
)
|
|
}
|
|
filenames.push(...mobileWebAssets)
|
|
return filenames
|
|
}
|
|
|
|
/** Files shared by every template target; the rest live under `targets/<target>/`. */
|
|
export function orcadTemplateCommonFilenames(mobileWebAssets: readonly string[]): string[] {
|
|
const filenames = ORCAD_ARTIFACTS.filter(
|
|
(artifact) =>
|
|
!artifact.optional &&
|
|
artifact.filename !== ORCAD_SERVER_TARGET_FILENAME &&
|
|
artifact.filename !== ORCAD_NODE_RUNTIME_MARKER_FILENAME &&
|
|
artifact.filename !== ORCAD_PARCEL_WATCHER_NATIVE
|
|
).map((artifact) => artifact.filename)
|
|
// The page is platform-independent, so the template keeps one copy for every target.
|
|
filenames.push(...mobileWebAssets)
|
|
return filenames
|
|
}
|
|
|
|
/** Files stored under the template's `targets/<target>/`, at their slot-relative paths. */
|
|
export function orcadTemplateTargetFilenames(target: string): string[] {
|
|
const common = new Set(orcadTemplateCommonFilenames([]))
|
|
return orcadArtifactFilenames(target, []).filter((filename) => !common.has(filename))
|
|
}
|