@fungi.computer/computer 0.0.0-stage → 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/Dockerfile +85 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/container/computer-init.mjs +119 -0
- package/container/computer-start.sh +24 -0
- package/container/package.json +14 -0
- package/container/pnpm-lock.yaml +378 -0
- package/container/pnpm-workspace.yaml +5 -0
- package/container/resource-bounds.mjs +54 -0
- package/container/runtime-privileges.cc +37 -0
- package/container/runtime-privileges.mjs +11 -0
- package/container/terminal-assets/README.md +35 -0
- package/container/terminal-assets/bashrc +49 -0
- package/container/terminal-assets/git-prompt.sh +586 -0
- package/container/terminal-protocol.mjs +58 -0
- package/container/terminal-runtime.mjs +10 -0
- package/container/terminal-server.mjs +243 -0
- package/dist/apply-patch/LICENSE.codex +201 -0
- package/dist/apply-patch/build.mjs +98 -0
- package/dist/apply-patch/cache.mjs +168 -0
- package/dist/apply-patch/compile.mjs +199 -0
- package/dist/apply-patch/docker/darwin.Dockerfile +15 -0
- package/dist/apply-patch/docker/linux-arm64.Dockerfile +38 -0
- package/dist/apply-patch/docker/linux-x64.Dockerfile +23 -0
- package/dist/apply-patch/main.rs +320 -0
- package/dist/apply-patch/notices.mjs +44 -0
- package/dist/apply-patch/pin.mjs +8 -0
- package/dist/apply-patch/toolchain.mjs +39 -0
- package/dist/computer-backends.d.ts +94 -0
- package/dist/computer-backends.js +103 -0
- package/dist/computerd.cjs +18010 -0
- package/dist/container-policy.d.ts +7 -0
- package/dist/container-policy.js +24 -0
- package/dist/container-system-trust.d.ts +8 -0
- package/dist/container-system-trust.js +66 -0
- package/dist/egress-policy.d.ts +89 -0
- package/dist/egress-policy.js +240 -0
- package/dist/execution-result.d.ts +3 -0
- package/dist/execution-result.js +28 -0
- package/dist/files-patch.d.ts +20 -0
- package/dist/files-patch.js +108 -0
- package/dist/files.d.ts +75 -0
- package/dist/files.js +136 -0
- package/dist/fleet/admission.d.ts +14 -0
- package/dist/fleet/admission.js +10 -0
- package/dist/fleet/backing.d.ts +52 -0
- package/dist/fleet/backing.js +242 -0
- package/dist/fleet/computers.d.ts +62 -0
- package/dist/fleet/computers.js +35 -0
- package/dist/fleet/index.d.ts +11 -0
- package/dist/fleet/index.js +11 -0
- package/dist/fleet/inventory-wire.d.ts +29 -0
- package/dist/fleet/inventory-wire.js +15 -0
- package/dist/fleet/inventory.d.ts +24 -0
- package/dist/fleet/inventory.js +152 -0
- package/dist/fleet/lifecycle.d.ts +99 -0
- package/dist/fleet/lifecycle.js +418 -0
- package/dist/fleet/runtime-usage.d.ts +50 -0
- package/dist/fleet/runtime-usage.js +145 -0
- package/dist/fleet/slots.d.ts +30 -0
- package/dist/fleet/slots.js +46 -0
- package/dist/fleet/wire.d.ts +228 -0
- package/dist/fleet/wire.js +127 -0
- package/dist/host-config.d.ts +12 -0
- package/dist/host-config.js +10 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +1237 -0
- package/dist/light-shell.d.ts +44 -0
- package/dist/light-shell.js +376 -0
- package/dist/local-backend.d.ts +52 -0
- package/dist/local-backend.js +143 -0
- package/dist/metered-container-host.d.ts +26 -0
- package/dist/metered-container-host.js +48 -0
- package/dist/on-box-vm.d.ts +119 -0
- package/dist/on-box-vm.js +294 -0
- package/dist/operation-contract.d.ts +456 -0
- package/dist/operation-contract.js +450 -0
- package/dist/operation-record.d.ts +50 -0
- package/dist/operation-record.js +162 -0
- package/dist/preview.d.ts +55 -0
- package/dist/preview.js +151 -0
- package/dist/process-inventory.d.ts +80 -0
- package/dist/process-inventory.js +427 -0
- package/dist/pull-pending.d.ts +26 -0
- package/dist/pull-pending.js +35 -0
- package/dist/team-candidate-builder.mjs +28220 -0
- package/dist/transport-backend.d.ts +3 -0
- package/dist/transport-backend.js +99 -0
- package/dist/workspace-stream.d.ts +2 -0
- package/dist/workspace-stream.js +31 -0
- package/package.json +126 -3
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
//! Fungi's rooted, bounded host for the pinned upstream Codex patch engine.
|
|
2
|
+
use codex_apply_patch::{
|
|
3
|
+
ApplyPatchError, ApplyPatchFileChange, ApplyPatchFileUpdateMode, ApplyPatchOptions, Hunk,
|
|
4
|
+
MaybeApplyPatchVerified, apply_patch_with_options, parse_patch, unified_diff_from_chunks,
|
|
5
|
+
verify_apply_patch_args_with_mode,
|
|
6
|
+
};
|
|
7
|
+
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
8
|
+
use codex_utils_path_uri::PathUri;
|
|
9
|
+
use std::cmp::Reverse;
|
|
10
|
+
use std::collections::HashSet;
|
|
11
|
+
use std::fmt;
|
|
12
|
+
use std::io::{self, Read};
|
|
13
|
+
#[cfg(unix)]
|
|
14
|
+
use std::os::unix::fs::MetadataExt;
|
|
15
|
+
use std::path::{Component, Path, PathBuf};
|
|
16
|
+
|
|
17
|
+
const MAX_BYTES: usize = 128 * 1024;
|
|
18
|
+
const MAX_FILES: usize = 256;
|
|
19
|
+
const MISMATCH_EXIT_CODE: i32 = 23;
|
|
20
|
+
const FORMAT_EXIT_CODE: i32 = 24;
|
|
21
|
+
|
|
22
|
+
fn expected_format(path: &Path) -> String {
|
|
23
|
+
format!(
|
|
24
|
+
"Expected patch format:\n*** Begin Patch\n*** Update File: {}\n@@\n-old line\n+new line\n*** End Patch\nUse @@ alone or @@ followed by an existing context line. Do not use unified-diff line numbers (such as @@ -1 +1 @@), --- or +++ file headers.\n",
|
|
25
|
+
preview(&path.to_string_lossy())
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[derive(Debug)]
|
|
30
|
+
struct PatchFormatError(String);
|
|
31
|
+
|
|
32
|
+
impl fmt::Display for PatchFormatError {
|
|
33
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
34
|
+
write!(
|
|
35
|
+
f,
|
|
36
|
+
"{}\n{}",
|
|
37
|
+
self.0,
|
|
38
|
+
expected_format(Path::new("path/to/file.txt"))
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl std::error::Error for PatchFormatError {}
|
|
44
|
+
|
|
45
|
+
#[derive(Debug)]
|
|
46
|
+
struct PatchMismatch {
|
|
47
|
+
path: PathBuf,
|
|
48
|
+
expected: Vec<String>,
|
|
49
|
+
actual: Vec<(usize, String)>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn preview(line: &str) -> String {
|
|
53
|
+
let mut chars = line.chars();
|
|
54
|
+
let value: String = chars.by_ref().take(160).collect();
|
|
55
|
+
if chars.next().is_some() {
|
|
56
|
+
format!("{value}…")
|
|
57
|
+
} else {
|
|
58
|
+
value
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl fmt::Display for PatchMismatch {
|
|
63
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
64
|
+
writeln!(
|
|
65
|
+
f,
|
|
66
|
+
"Patch hunk does not match {}.",
|
|
67
|
+
preview(&self.path.to_string_lossy())
|
|
68
|
+
)?;
|
|
69
|
+
writeln!(f, "Expected hunk:")?;
|
|
70
|
+
for line in self.expected.iter().take(5) {
|
|
71
|
+
writeln!(f, " {}", preview(line))?;
|
|
72
|
+
}
|
|
73
|
+
if self.expected.len() > 5 {
|
|
74
|
+
writeln!(f, " … {} more expected lines", self.expected.len() - 5)?;
|
|
75
|
+
}
|
|
76
|
+
writeln!(f, "Closest actual lines:")?;
|
|
77
|
+
for (number, line) in &self.actual {
|
|
78
|
+
writeln!(f, " {number}: {}", preview(line))?;
|
|
79
|
+
}
|
|
80
|
+
writeln!(
|
|
81
|
+
f,
|
|
82
|
+
"Use the current lines above to correct this hunk and retry."
|
|
83
|
+
)?;
|
|
84
|
+
write!(f, "{}", expected_format(&self.path))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
impl std::error::Error for PatchMismatch {}
|
|
89
|
+
|
|
90
|
+
fn shared_edges(expected: &str, actual: &str) -> usize {
|
|
91
|
+
let prefix = expected
|
|
92
|
+
.chars()
|
|
93
|
+
.zip(actual.chars())
|
|
94
|
+
.take_while(|(left, right)| left == right)
|
|
95
|
+
.count();
|
|
96
|
+
let suffix = expected
|
|
97
|
+
.chars()
|
|
98
|
+
.rev()
|
|
99
|
+
.zip(actual.chars().rev())
|
|
100
|
+
.take_while(|(left, right)| left == right)
|
|
101
|
+
.count();
|
|
102
|
+
prefix + suffix
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async fn first_mismatch(root: &Path, hunks: &[Hunk]) -> Option<PatchMismatch> {
|
|
106
|
+
for hunk in hunks {
|
|
107
|
+
let Hunk::UpdateFile { path, chunks, .. } = hunk else {
|
|
108
|
+
continue;
|
|
109
|
+
};
|
|
110
|
+
let target = admit(root, path).ok()?;
|
|
111
|
+
let source = std::fs::read_to_string(&target).ok()?;
|
|
112
|
+
let uri = PathUri::from_abs_path(&AbsolutePathBuf::try_from(target).ok()?);
|
|
113
|
+
let lines: Vec<&str> = source.lines().collect();
|
|
114
|
+
for (index, chunk) in chunks.iter().enumerate() {
|
|
115
|
+
// Let the engine identify the first failing chunk, including
|
|
116
|
+
// anchors, EOF and ordering. Never duplicate its matching rules.
|
|
117
|
+
if unified_diff_from_chunks(
|
|
118
|
+
&uri,
|
|
119
|
+
&chunks[..=index],
|
|
120
|
+
codex_exec_server::LOCAL_FS.as_ref(),
|
|
121
|
+
None,
|
|
122
|
+
)
|
|
123
|
+
.await
|
|
124
|
+
.is_ok()
|
|
125
|
+
{
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let expected = if chunk.old_lines.is_empty() {
|
|
129
|
+
chunk.change_context.iter().cloned().collect()
|
|
130
|
+
} else {
|
|
131
|
+
chunk.old_lines.clone()
|
|
132
|
+
};
|
|
133
|
+
let nearest = (0..lines.len())
|
|
134
|
+
.max_by_key(|&start| {
|
|
135
|
+
let score = expected
|
|
136
|
+
.iter()
|
|
137
|
+
.enumerate()
|
|
138
|
+
.map(|(offset, old)| {
|
|
139
|
+
lines
|
|
140
|
+
.get(start + offset)
|
|
141
|
+
.map_or(0, |actual| shared_edges(old, actual))
|
|
142
|
+
})
|
|
143
|
+
.sum::<usize>();
|
|
144
|
+
(score, Reverse(start))
|
|
145
|
+
})
|
|
146
|
+
.unwrap_or(0);
|
|
147
|
+
let from = nearest.saturating_sub(2);
|
|
148
|
+
let to = (nearest + expected.len() + 2)
|
|
149
|
+
.min(lines.len())
|
|
150
|
+
.min(from + 7);
|
|
151
|
+
return Some(PatchMismatch {
|
|
152
|
+
path: path.clone(),
|
|
153
|
+
expected,
|
|
154
|
+
actual: (from..to)
|
|
155
|
+
.map(|index| (index + 1, lines[index].to_owned()))
|
|
156
|
+
.collect(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
None
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn admit(root: &Path, path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
164
|
+
let path = if path.is_absolute() {
|
|
165
|
+
path.to_path_buf()
|
|
166
|
+
} else {
|
|
167
|
+
root.join(path)
|
|
168
|
+
};
|
|
169
|
+
let relative = path
|
|
170
|
+
.strip_prefix(root)
|
|
171
|
+
.map_err(|_| "Patch path is outside the invocation root")?;
|
|
172
|
+
if relative.as_os_str().is_empty() {
|
|
173
|
+
return Err("Patch cannot replace the invocation root".into());
|
|
174
|
+
}
|
|
175
|
+
let mut current = root.to_path_buf();
|
|
176
|
+
for component in relative.components() {
|
|
177
|
+
match component {
|
|
178
|
+
Component::Normal(part) => current.push(part),
|
|
179
|
+
Component::CurDir => continue,
|
|
180
|
+
_ => return Err("Patch paths cannot contain parent traversal".into()),
|
|
181
|
+
}
|
|
182
|
+
match std::fs::symlink_metadata(¤t) {
|
|
183
|
+
Ok(meta) => {
|
|
184
|
+
if meta.file_type().is_symlink() {
|
|
185
|
+
return Err(
|
|
186
|
+
format!("Patch cannot follow symlinks: {}", current.display()).into(),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if !meta.is_dir() && !meta.is_file() {
|
|
190
|
+
return Err("Patch requires regular files".into());
|
|
191
|
+
}
|
|
192
|
+
#[cfg(unix)]
|
|
193
|
+
if meta.is_file() && meta.nlink() > 1 {
|
|
194
|
+
return Err(format!(
|
|
195
|
+
"Patch cannot modify hard-linked file {}. Make a private copy in this folder (copy to a new file, then rename it over this path) and retry; files.edit can replace files under 128 KiB.",
|
|
196
|
+
current.display()
|
|
197
|
+
).into());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
|
201
|
+
Err(error) => return Err(error.into()),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
Ok(current)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
208
|
+
let root = std::fs::canonicalize(std::env::current_dir()?)?;
|
|
209
|
+
let mut patch = String::new();
|
|
210
|
+
io::stdin()
|
|
211
|
+
.take((MAX_BYTES + 1) as u64)
|
|
212
|
+
.read_to_string(&mut patch)?;
|
|
213
|
+
if patch.len() > MAX_BYTES {
|
|
214
|
+
return Err("Patch exceeds the 128 KiB input bound".into());
|
|
215
|
+
}
|
|
216
|
+
let parsed = parse_patch(&patch).map_err(|error| PatchFormatError(error.to_string()))?;
|
|
217
|
+
if parsed.environment_id.is_some() || parsed.workdir.is_some() {
|
|
218
|
+
return Err("Patch cannot select another environment or working directory".into());
|
|
219
|
+
}
|
|
220
|
+
if parsed.hunks.is_empty() || parsed.hunks.len() > MAX_FILES {
|
|
221
|
+
return Err("Patch must affect between 1 and 256 files".into());
|
|
222
|
+
}
|
|
223
|
+
// Verification reads one pre-patch snapshot. Overlapping source/destination
|
|
224
|
+
// paths could otherwise invalidate its result-size checks during application.
|
|
225
|
+
let mut paths = HashSet::new();
|
|
226
|
+
let mut reserve = |path: &Path| -> Result<(), Box<dyn std::error::Error>> {
|
|
227
|
+
if !paths.insert(admit(&root, path)?) {
|
|
228
|
+
return Err("Patch operations have overlapping source or move paths".into());
|
|
229
|
+
}
|
|
230
|
+
Ok(())
|
|
231
|
+
};
|
|
232
|
+
for hunk in &parsed.hunks {
|
|
233
|
+
match hunk {
|
|
234
|
+
Hunk::AddFile { path, .. } | Hunk::DeleteFile { path } => reserve(path)?,
|
|
235
|
+
Hunk::UpdateFile {
|
|
236
|
+
path, move_path, ..
|
|
237
|
+
} => {
|
|
238
|
+
reserve(path)?;
|
|
239
|
+
if let Some(destination) = move_path {
|
|
240
|
+
reserve(destination)?;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
let cwd = PathUri::from_abs_path(&AbsolutePathBuf::try_from(PathBuf::from(&root))?);
|
|
246
|
+
let mode = ApplyPatchFileUpdateMode::PreserveLineEndings;
|
|
247
|
+
let diagnostic_hunks = parsed.hunks.clone();
|
|
248
|
+
let action = match verify_apply_patch_args_with_mode(
|
|
249
|
+
parsed,
|
|
250
|
+
&cwd,
|
|
251
|
+
mode,
|
|
252
|
+
codex_exec_server::LOCAL_FS.as_ref(),
|
|
253
|
+
None,
|
|
254
|
+
)
|
|
255
|
+
.await
|
|
256
|
+
{
|
|
257
|
+
MaybeApplyPatchVerified::Body(action) => action,
|
|
258
|
+
MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ComputeReplacements(error)) => {
|
|
259
|
+
if let Some(mismatch) = first_mismatch(&root, &diagnostic_hunks).await {
|
|
260
|
+
return Err(Box::new(mismatch));
|
|
261
|
+
}
|
|
262
|
+
return Err(format!("Patch verification failed: {error}").into());
|
|
263
|
+
}
|
|
264
|
+
MaybeApplyPatchVerified::CorrectnessError(error) => {
|
|
265
|
+
return Err(format!("Patch verification failed: {error}").into());
|
|
266
|
+
}
|
|
267
|
+
MaybeApplyPatchVerified::ShellParseError(_) => {
|
|
268
|
+
return Err("Patch syntax error".into());
|
|
269
|
+
}
|
|
270
|
+
MaybeApplyPatchVerified::NotApplyPatch => {
|
|
271
|
+
return Err("Patch input was not an apply_patch call".into());
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
for change in action.changes().values() {
|
|
275
|
+
let changed_bytes = match change {
|
|
276
|
+
ApplyPatchFileChange::Add { content } | ApplyPatchFileChange::Delete { content } => {
|
|
277
|
+
content.len()
|
|
278
|
+
}
|
|
279
|
+
ApplyPatchFileChange::Update { unified_diff, .. } => unified_diff.len(),
|
|
280
|
+
};
|
|
281
|
+
if changed_bytes > MAX_BYTES {
|
|
282
|
+
return Err("Patch change exceeds the 128 KiB edit bound".into());
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
apply_patch_with_options(
|
|
286
|
+
&patch,
|
|
287
|
+
ApplyPatchOptions {
|
|
288
|
+
update_file_mode: mode,
|
|
289
|
+
follow_symlinks: false,
|
|
290
|
+
},
|
|
291
|
+
&cwd,
|
|
292
|
+
&mut io::stdout(),
|
|
293
|
+
&mut io::stderr(),
|
|
294
|
+
codex_exec_server::LOCAL_FS.as_ref(),
|
|
295
|
+
None,
|
|
296
|
+
)
|
|
297
|
+
.await
|
|
298
|
+
.map_err(|failure| {
|
|
299
|
+
let (error, delta) = failure.into_parts();
|
|
300
|
+
format!(
|
|
301
|
+
"{error}; {} file changes already applied (no rollback)",
|
|
302
|
+
delta.changes().len()
|
|
303
|
+
)
|
|
304
|
+
})?;
|
|
305
|
+
Ok(())
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[tokio::main(flavor = "current_thread")]
|
|
309
|
+
async fn main() {
|
|
310
|
+
if let Err(error) = run().await {
|
|
311
|
+
eprintln!("{error}");
|
|
312
|
+
std::process::exit(if error.is::<PatchMismatch>() {
|
|
313
|
+
MISMATCH_EXIT_CODE
|
|
314
|
+
} else if error.is::<PatchFormatError>() {
|
|
315
|
+
FORMAT_EXIT_CODE
|
|
316
|
+
} else {
|
|
317
|
+
1
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const noticeName =
|
|
6
|
+
/^(licen[sc]es?|copying|copyright|notice|third.party.notices)([._-]|$)/i;
|
|
7
|
+
|
|
8
|
+
export async function noticeFiles(directory) {
|
|
9
|
+
const result = [];
|
|
10
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
11
|
+
if (!noticeName.test(entry.name)) continue;
|
|
12
|
+
const path = resolve(directory, entry.name);
|
|
13
|
+
if (entry.isFile()) result.push(path);
|
|
14
|
+
else if (entry.isDirectory()) {
|
|
15
|
+
for (const child of await readdir(path, { withFileTypes: true })) {
|
|
16
|
+
if (child.isFile()) result.push(resolve(path, child.name));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result.sort();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Cache the actual compiler closure's texts with its binary. Absolute Cargo
|
|
24
|
+
// source paths cannot serve as packaging inputs after a cross-worktree cache hit.
|
|
25
|
+
export async function nativeNotices(artifacts, sourceRoot) {
|
|
26
|
+
const packages = new Map();
|
|
27
|
+
for (const artifact of artifacts) {
|
|
28
|
+
const name = new URL(artifact.package_id).hash.slice(1);
|
|
29
|
+
if (!name) throw new Error("Native artifact has no Cargo package identity");
|
|
30
|
+
const directory = dirname(artifact.manifest_path);
|
|
31
|
+
const root = directory.startsWith(sourceRoot + "/")
|
|
32
|
+
? sourceRoot
|
|
33
|
+
: directory;
|
|
34
|
+
packages.set(name, {
|
|
35
|
+
name,
|
|
36
|
+
texts: await Promise.all(
|
|
37
|
+
(await noticeFiles(root)).map((file) => readFile(file, "utf8")),
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return [...packages.values()].sort((left, right) =>
|
|
42
|
+
left.name.localeCompare(right.name),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The shared artifact identity; this directory is a native Turbo input. */
|
|
2
|
+
export const codexPatchPin = Object.freeze({
|
|
3
|
+
version: "rust-v0.154.0",
|
|
4
|
+
sourceSha256:
|
|
5
|
+
"1c4cdc3b87ba290b5d110425b4f6ff21663e236580bc760d1e149bd2d9f9519f",
|
|
6
|
+
/** The Rust toolchain every builder (host, container, image stage) uses. */
|
|
7
|
+
rustToolchain: "1.98.1",
|
|
8
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Selects the pinned Rust toolchain's `cargo` for the native patch build.
|
|
2
|
+
//
|
|
3
|
+
// Every builder (a native host, the release containers and the Computer
|
|
4
|
+
// image's builder stage) installs Rust through rustup. A non-rustup `cargo`
|
|
5
|
+
// earlier on `PATH` (for example Homebrew's) rejects the `+toolchain`
|
|
6
|
+
// directive, so the build asks rustup's ordinary interface for the pinned
|
|
7
|
+
// toolchain's `cargo` explicitly.
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds the `{ command, args }` cargo invocation for the pinned toolchain.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ toolchain: string, cargoArgs: string[] }} options
|
|
14
|
+
*/
|
|
15
|
+
export function selectCargoInvocation({ toolchain, cargoArgs }) {
|
|
16
|
+
return { command: "rustup", args: ["run", toolchain, "cargo", ...cargoArgs] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Confirms rustup and the pinned toolchain are available before starting the
|
|
21
|
+
* real build, so a missing prerequisite fails with one actionable message
|
|
22
|
+
* naming the fix instead of an opaque nested build error.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ toolchain: string }} options
|
|
25
|
+
*/
|
|
26
|
+
export function ensureNativeHostToolchain({ toolchain }) {
|
|
27
|
+
const probe = spawnSync("rustup", ["run", toolchain, "cargo", "--version"], {
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
});
|
|
30
|
+
if (probe.error?.code === "ENOENT")
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Cordyceps' native patch build needs rustup with the Rust ${toolchain} toolchain. Install rustup from https://rustup.rs, then run \`rustup toolchain install ${toolchain}\`.`,
|
|
33
|
+
);
|
|
34
|
+
if (probe.error) throw probe.error;
|
|
35
|
+
if (probe.status !== 0)
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Cordyceps' native patch build needs the Rust ${toolchain} toolchain through rustup. Run \`rustup toolchain install ${toolchain}\`. rustup reported: ${probe.stderr.trim()}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/** A backend the platform runs: a Cloudflare container or an on-box VM. */
|
|
3
|
+
export declare const hostedBackingSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
4
|
+
kind: z.ZodLiteral<"platform-container">;
|
|
5
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
6
|
+
kind: z.ZodLiteral<"on-box-vm">;
|
|
7
|
+
hostId: z.ZodLazy<z.ZodString>;
|
|
8
|
+
}, z.core.$strict>], "kind">;
|
|
9
|
+
export type HostedBacking = z.infer<typeof hostedBackingSchema>;
|
|
10
|
+
/** A member's machine, attached with a device key that never leaves it. */
|
|
11
|
+
export declare const localBackendSchema: z.ZodObject<{
|
|
12
|
+
kind: z.ZodLiteral<"local">;
|
|
13
|
+
deviceId: z.ZodString;
|
|
14
|
+
publicKey: z.ZodBase64;
|
|
15
|
+
hostLabel: z.ZodLazy<z.ZodString>;
|
|
16
|
+
folderLabel: z.ZodLazy<z.ZodString>;
|
|
17
|
+
}, z.core.$strict>;
|
|
18
|
+
export type LocalBackend = z.infer<typeof localBackendSchema>;
|
|
19
|
+
export declare const attachedBackendSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
20
|
+
kind: z.ZodLiteral<"platform-container">;
|
|
21
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
22
|
+
kind: z.ZodLiteral<"on-box-vm">;
|
|
23
|
+
hostId: z.ZodLazy<z.ZodString>;
|
|
24
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
25
|
+
kind: z.ZodLiteral<"local">;
|
|
26
|
+
deviceId: z.ZodString;
|
|
27
|
+
publicKey: z.ZodBase64;
|
|
28
|
+
hostLabel: z.ZodLazy<z.ZodString>;
|
|
29
|
+
folderLabel: z.ZodLazy<z.ZodString>;
|
|
30
|
+
}, z.core.$strict>], "kind">;
|
|
31
|
+
export type AttachedBackend = z.infer<typeof attachedBackendSchema>;
|
|
32
|
+
/** The stable Workspace backend id: its sync cursor is keyed by it. */
|
|
33
|
+
export declare function attachedBackendId(backend: AttachedBackend): string;
|
|
34
|
+
/** One Computer's attached backends, in attach order; ids are unique. */
|
|
35
|
+
export declare const attachedBackendsSchema: z.ZodLazy<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
36
|
+
kind: z.ZodLiteral<"platform-container">;
|
|
37
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
38
|
+
kind: z.ZodLiteral<"on-box-vm">;
|
|
39
|
+
hostId: z.ZodLazy<z.ZodString>;
|
|
40
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
41
|
+
kind: z.ZodLiteral<"local">;
|
|
42
|
+
deviceId: z.ZodString;
|
|
43
|
+
publicKey: z.ZodBase64;
|
|
44
|
+
hostLabel: z.ZodLazy<z.ZodString>;
|
|
45
|
+
folderLabel: z.ZodLazy<z.ZodString>;
|
|
46
|
+
}, z.core.$strict>], "kind">>>;
|
|
47
|
+
export declare const attachedBackendIdSchema: z.ZodLazy<z.ZodString>;
|
|
48
|
+
export type AttachedBackendStatus = "ready" | "online" | "offline";
|
|
49
|
+
/** What a Computer reports about one attached backend. */
|
|
50
|
+
export type AttachedBackendView = Readonly<{
|
|
51
|
+
id: string;
|
|
52
|
+
kind: AttachedBackend["kind"];
|
|
53
|
+
status: AttachedBackendStatus;
|
|
54
|
+
hostId?: string;
|
|
55
|
+
hostLabel?: string;
|
|
56
|
+
folderLabel?: string;
|
|
57
|
+
}>;
|
|
58
|
+
export declare function describeAttachedBackend(backend: AttachedBackend, online: boolean): AttachedBackendView;
|
|
59
|
+
/** What a command needs from its backend. */
|
|
60
|
+
export type BackendNeed =
|
|
61
|
+
/** A shell over the Computer's files: any attached backend. */
|
|
62
|
+
"exec"
|
|
63
|
+
/** Terminal, processes or ports: a platform-run backend only. */
|
|
64
|
+
| "hosted";
|
|
65
|
+
export type BackendRoute = Readonly<{
|
|
66
|
+
kind: "routed";
|
|
67
|
+
id: string;
|
|
68
|
+
backend: AttachedBackend;
|
|
69
|
+
}>
|
|
70
|
+
/** The named backend is not attached, cannot serve the need, or is offline. */
|
|
71
|
+
| Readonly<{
|
|
72
|
+
kind: "backend_unavailable";
|
|
73
|
+
id: string;
|
|
74
|
+
}>
|
|
75
|
+
/** No attached backend can serve the command now. */
|
|
76
|
+
| Readonly<{
|
|
77
|
+
kind: "no_backend";
|
|
78
|
+
}>;
|
|
79
|
+
/**
|
|
80
|
+
* The single routing decision. An explicit id selects exactly that backend
|
|
81
|
+
* and never falls back. Otherwise the default order is online local backends,
|
|
82
|
+
* then on-box VMs, then the platform container, each kind in attach order.
|
|
83
|
+
* Hosted backends start on demand, so only a local backend can be offline.
|
|
84
|
+
*/
|
|
85
|
+
export declare function routeComputerBackend(backends: readonly AttachedBackend[], online: (id: string) => boolean, request: Readonly<{
|
|
86
|
+
backend?: string | undefined;
|
|
87
|
+
need: BackendNeed;
|
|
88
|
+
}>): BackendRoute;
|
|
89
|
+
/** One container runtime a hosted backend started: the Computer-hours unit. */
|
|
90
|
+
export type ContainerRuntimeStart = Readonly<{
|
|
91
|
+
runtimeId: string;
|
|
92
|
+
backing: HostedBacking;
|
|
93
|
+
startedAt: number;
|
|
94
|
+
}>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* A Computer is its SQLite Workspace. Execution backends are optional and
|
|
4
|
+
* plural: this module owns their vocabulary, their stable Workspace backend
|
|
5
|
+
* ids, and the one routing decision that picks a backend for a command.
|
|
6
|
+
*
|
|
7
|
+
* Every shape here is secret-free. A hosted backing names a registered host
|
|
8
|
+
* id at most; a local backend carries its device's public key and two display
|
|
9
|
+
* labels, never a path on the member's machine.
|
|
10
|
+
*/
|
|
11
|
+
const hostIdSchema = z.lazy(() => z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/));
|
|
12
|
+
const labelSchema = z.lazy(() => z.string().trim().min(1).max(256));
|
|
13
|
+
/** A backend the platform runs: a Cloudflare container or an on-box VM. */
|
|
14
|
+
export const hostedBackingSchema = z.discriminatedUnion("kind", [
|
|
15
|
+
z.strictObject({ kind: z.literal("platform-container") }),
|
|
16
|
+
z.strictObject({ kind: z.literal("on-box-vm"), hostId: hostIdSchema }),
|
|
17
|
+
]);
|
|
18
|
+
/** A member's machine, attached with a device key that never leaves it. */
|
|
19
|
+
export const localBackendSchema = z.strictObject({
|
|
20
|
+
kind: z.literal("local"),
|
|
21
|
+
deviceId: z.string().regex(/^[a-z2-7]{16}$/),
|
|
22
|
+
/** Raw 32-byte Ed25519 public key, base64. */
|
|
23
|
+
publicKey: z.base64().length(44),
|
|
24
|
+
hostLabel: labelSchema,
|
|
25
|
+
folderLabel: labelSchema,
|
|
26
|
+
});
|
|
27
|
+
export const attachedBackendSchema = z.discriminatedUnion("kind", [
|
|
28
|
+
...hostedBackingSchema.options,
|
|
29
|
+
localBackendSchema,
|
|
30
|
+
]);
|
|
31
|
+
/** The stable Workspace backend id: its sync cursor is keyed by it. */
|
|
32
|
+
export function attachedBackendId(backend) {
|
|
33
|
+
switch (backend.kind) {
|
|
34
|
+
case "platform-container":
|
|
35
|
+
return "container";
|
|
36
|
+
case "on-box-vm":
|
|
37
|
+
return `vm-${backend.hostId}`;
|
|
38
|
+
case "local":
|
|
39
|
+
return `local-${backend.deviceId}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** One Computer's attached backends, in attach order; ids are unique. */
|
|
43
|
+
export const attachedBackendsSchema = z.lazy(() => z
|
|
44
|
+
.array(attachedBackendSchema)
|
|
45
|
+
.max(16)
|
|
46
|
+
.refine((backends) => new Set(backends.map(attachedBackendId)).size === backends.length, "attached backend ids must be unique"));
|
|
47
|
+
export const attachedBackendIdSchema = z.lazy(() => z
|
|
48
|
+
.string()
|
|
49
|
+
.regex(/^(container|vm-[a-z0-9][a-z0-9-]{0,62}|local-[a-z2-7]{16})$/));
|
|
50
|
+
export function describeAttachedBackend(backend, online) {
|
|
51
|
+
const id = attachedBackendId(backend);
|
|
52
|
+
switch (backend.kind) {
|
|
53
|
+
case "platform-container":
|
|
54
|
+
return { id, kind: backend.kind, status: "ready" };
|
|
55
|
+
case "on-box-vm":
|
|
56
|
+
return {
|
|
57
|
+
id,
|
|
58
|
+
kind: backend.kind,
|
|
59
|
+
status: "ready",
|
|
60
|
+
hostId: backend.hostId,
|
|
61
|
+
};
|
|
62
|
+
case "local":
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
kind: backend.kind,
|
|
66
|
+
status: online ? "online" : "offline",
|
|
67
|
+
hostLabel: backend.hostLabel,
|
|
68
|
+
folderLabel: backend.folderLabel,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const defaultRank = {
|
|
73
|
+
local: 0,
|
|
74
|
+
"on-box-vm": 1,
|
|
75
|
+
"platform-container": 2,
|
|
76
|
+
};
|
|
77
|
+
function serves(backend, need) {
|
|
78
|
+
return need === "exec" || backend.kind !== "local";
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The single routing decision. An explicit id selects exactly that backend
|
|
82
|
+
* and never falls back. Otherwise the default order is online local backends,
|
|
83
|
+
* then on-box VMs, then the platform container, each kind in attach order.
|
|
84
|
+
* Hosted backends start on demand, so only a local backend can be offline.
|
|
85
|
+
*/
|
|
86
|
+
export function routeComputerBackend(backends, online, request) {
|
|
87
|
+
const available = (backend) => serves(backend, request.need) &&
|
|
88
|
+
(backend.kind !== "local" || online(attachedBackendId(backend)));
|
|
89
|
+
if (request.backend !== undefined) {
|
|
90
|
+
const backend = backends.find((candidate) => attachedBackendId(candidate) === request.backend);
|
|
91
|
+
return backend !== undefined && available(backend)
|
|
92
|
+
? { kind: "routed", id: request.backend, backend }
|
|
93
|
+
: { kind: "backend_unavailable", id: request.backend };
|
|
94
|
+
}
|
|
95
|
+
const chosen = backends
|
|
96
|
+
.map((backend, index) => ({ backend, index }))
|
|
97
|
+
.filter(({ backend }) => available(backend))
|
|
98
|
+
.sort((left, right) => defaultRank[left.backend.kind] - defaultRank[right.backend.kind] ||
|
|
99
|
+
left.index - right.index)[0]?.backend;
|
|
100
|
+
return chosen === undefined
|
|
101
|
+
? { kind: "no_backend" }
|
|
102
|
+
: { kind: "routed", id: attachedBackendId(chosen), backend: chosen };
|
|
103
|
+
}
|