@lamentis/naome 1.3.7 → 1.3.9
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/Cargo.lock +2 -2
- package/README.md +5 -0
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/install_bridge.rs +56 -8
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/context/select.rs +58 -4
- package/crates/naome-core/src/harness_health/integrity.rs +41 -23
- package/crates/naome-core/src/harness_health/manifest.rs +97 -0
- package/crates/naome-core/src/harness_health.rs +58 -106
- package/crates/naome-core/src/intent/classifier.rs +56 -81
- package/crates/naome-core/src/intent/envelope.rs +173 -19
- package/crates/naome-core/src/intent/legacy_response.rs +2 -0
- package/crates/naome-core/src/intent/model.rs +6 -0
- package/crates/naome-core/src/intent/resolver.rs +25 -0
- package/crates/naome-core/src/intent/risk.rs +11 -1
- package/crates/naome-core/src/intent.rs +1 -1
- package/crates/naome-core/src/quality/cache.rs +122 -19
- package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
- package/crates/naome-core/src/quality/scanner.rs +5 -2
- package/crates/naome-core/src/route/context.rs +8 -0
- package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
- package/crates/naome-core/tests/context.rs +92 -0
- package/crates/naome-core/tests/harness_health.rs +149 -0
- package/crates/naome-core/tests/intent.rs +98 -18
- package/crates/naome-core/tests/intent_support/mod.rs +39 -1
- package/crates/naome-core/tests/intent_v2.rs +299 -10
- package/crates/naome-core/tests/quality_performance.rs +63 -2
- package/crates/naome-core/tests/repo_support/routes.rs +8 -2
- package/crates/naome-core/tests/route_baseline.rs +29 -0
- package/crates/naome-core/tests/route_completion.rs +26 -5
- package/crates/naome-core/tests/route_harness_refresh.rs +7 -1
- package/crates/naome-core/tests/route_user_diff.rs +1 -1
- package/crates/naome-core/tests/task_state_compact.rs +7 -1
- package/installer/filesystem.js +38 -0
- package/installer/flows.js +6 -1
- package/installer/harness-file-ops.js +36 -8
- package/installer/manifest-state.js +2 -2
- package/installer/native.js +63 -18
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +25 -21
- package/templates/naome-root/.naome/bin/check-task-state.js +35 -42
- package/templates/naome-root/.naome/manifest.json +10 -10
- package/templates/naome-root/docs/naome/agent-workflow.md +14 -5
- package/templates/naome-root/docs/naome/architecture.md +9 -0
- package/crates/naome-core/src/intent/patterns.rs +0 -170
package/Cargo.lock
CHANGED
|
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
76
76
|
|
|
77
77
|
[[package]]
|
|
78
78
|
name = "naome-cli"
|
|
79
|
-
version = "1.3.
|
|
79
|
+
version = "1.3.9"
|
|
80
80
|
dependencies = [
|
|
81
81
|
"naome-core",
|
|
82
82
|
"serde_json",
|
|
@@ -84,7 +84,7 @@ dependencies = [
|
|
|
84
84
|
|
|
85
85
|
[[package]]
|
|
86
86
|
name = "naome-core"
|
|
87
|
-
version = "1.3.
|
|
87
|
+
version = "1.3.9"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
package/README.md
CHANGED
|
@@ -41,6 +41,11 @@ For agent-driven work, route the user's request through the harness:
|
|
|
41
41
|
naome route --prompt-file /path/to/prompt.txt --execute --json
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
Prompt files should start with a fenced `naome-prompt-envelope-v1` JSON
|
|
45
|
+
envelope that normalizes the raw user text into canonical routing fields. A raw
|
|
46
|
+
natural-language prompt routes to a non-mutating normalization decision instead
|
|
47
|
+
of becoming a task by keyword inference.
|
|
48
|
+
|
|
44
49
|
## Why NAOME?
|
|
45
50
|
|
|
46
51
|
- Keeps agents inside explicit task scope.
|
|
@@ -10,8 +10,7 @@ pub fn run_install_bridge(
|
|
|
10
10
|
let package_root = option_value(args, "--package-root")
|
|
11
11
|
.map(PathBuf::from)
|
|
12
12
|
.or_else(|| std::env::var("NAOME_PACKAGE_ROOT").ok().map(PathBuf::from))
|
|
13
|
-
.or_else(resolve_package_root_from_exe)
|
|
14
|
-
.or_else(resolve_package_root_from_cwd);
|
|
13
|
+
.or_else(resolve_package_root_from_exe);
|
|
15
14
|
let installer_js = option_value(args, "--installer-js")
|
|
16
15
|
.map(PathBuf::from)
|
|
17
16
|
.or_else(|| std::env::var("NAOME_INSTALLER_JS").ok().map(PathBuf::from))
|
|
@@ -72,12 +71,61 @@ fn resolve_package_root_from_exe() -> Option<PathBuf> {
|
|
|
72
71
|
None
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
#[cfg(test)]
|
|
75
|
+
mod tests {
|
|
76
|
+
use std::fs;
|
|
77
|
+
use std::sync::{Mutex, OnceLock};
|
|
78
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
79
|
+
|
|
80
|
+
use super::run_install_bridge;
|
|
81
|
+
|
|
82
|
+
fn env_lock() -> &'static Mutex<()> {
|
|
83
|
+
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
|
84
|
+
LOCK.get_or_init(|| Mutex::new(()))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn install_bridge_does_not_discover_installer_from_cwd() {
|
|
89
|
+
let _guard = env_lock().lock().unwrap();
|
|
90
|
+
let original_dir = std::env::current_dir().unwrap();
|
|
91
|
+
let original_package_root = std::env::var_os("NAOME_PACKAGE_ROOT");
|
|
92
|
+
let original_installer_js = std::env::var_os("NAOME_INSTALLER_JS");
|
|
93
|
+
let temp_root = std::env::temp_dir().join(format!(
|
|
94
|
+
"naome-cwd-installer-test-{}",
|
|
95
|
+
SystemTime::now()
|
|
96
|
+
.duration_since(UNIX_EPOCH)
|
|
97
|
+
.unwrap()
|
|
98
|
+
.as_nanos()
|
|
99
|
+
));
|
|
100
|
+
let installer_path = temp_root
|
|
101
|
+
.join("packages")
|
|
102
|
+
.join("naome")
|
|
103
|
+
.join("bin")
|
|
104
|
+
.join("naome-node.js");
|
|
105
|
+
fs::create_dir_all(installer_path.parent().unwrap()).unwrap();
|
|
106
|
+
fs::write(&installer_path, "console.log('untrusted cwd installer');\n").unwrap();
|
|
107
|
+
|
|
108
|
+
std::env::remove_var("NAOME_PACKAGE_ROOT");
|
|
109
|
+
std::env::remove_var("NAOME_INSTALLER_JS");
|
|
110
|
+
std::env::set_current_dir(&temp_root).unwrap();
|
|
111
|
+
|
|
112
|
+
let args = vec!["install".to_string()];
|
|
113
|
+
let error = run_install_bridge("install", &args)
|
|
114
|
+
.expect_err("cwd-local installer must not be treated as trusted");
|
|
115
|
+
assert!(
|
|
116
|
+
error.to_string().contains("needs naome-node.js"),
|
|
117
|
+
"unexpected error: {error}"
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
std::env::set_current_dir(original_dir).unwrap();
|
|
121
|
+
match original_package_root {
|
|
122
|
+
Some(value) => std::env::set_var("NAOME_PACKAGE_ROOT", value),
|
|
123
|
+
None => std::env::remove_var("NAOME_PACKAGE_ROOT"),
|
|
80
124
|
}
|
|
125
|
+
match original_installer_js {
|
|
126
|
+
Some(value) => std::env::set_var("NAOME_INSTALLER_JS", value),
|
|
127
|
+
None => std::env::remove_var("NAOME_INSTALLER_JS"),
|
|
128
|
+
}
|
|
129
|
+
let _ = fs::remove_dir_all(temp_root);
|
|
81
130
|
}
|
|
82
|
-
None
|
|
83
131
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
use std::path::Path;
|
|
1
|
+
use std::path::{Component, Path};
|
|
2
2
|
|
|
3
|
+
use serde::Deserialize;
|
|
4
|
+
|
|
5
|
+
use crate::intent::prompt_envelope_json;
|
|
3
6
|
use crate::{git, models::NaomeError};
|
|
4
7
|
|
|
5
8
|
use super::helpers::{capsule_for_paths, context_item, is_source_path, normalize_paths};
|
|
@@ -7,6 +10,13 @@ use super::types::{ContextBudgetLedger, ContextItem, ContextSelection};
|
|
|
7
10
|
|
|
8
11
|
const MAX_CONTEXT_FILES: usize = 12;
|
|
9
12
|
|
|
13
|
+
#[derive(Debug, Deserialize)]
|
|
14
|
+
#[serde(rename_all = "camelCase")]
|
|
15
|
+
struct PromptEnvelopeContext {
|
|
16
|
+
schema: Option<String>,
|
|
17
|
+
referenced_paths: Option<Vec<String>>,
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
pub fn select_context_for_changed_paths(root: &Path) -> Result<ContextSelection, NaomeError> {
|
|
11
21
|
let paths = git::changed_paths(root)?;
|
|
12
22
|
Ok(selection_for_paths(root, "changed", paths))
|
|
@@ -19,7 +29,9 @@ pub fn select_context_for_prompt(
|
|
|
19
29
|
Ok(selection_for_paths(
|
|
20
30
|
root,
|
|
21
31
|
"prompt",
|
|
22
|
-
|
|
32
|
+
envelope_referenced_paths(prompt.as_ref())
|
|
33
|
+
.filter(|paths| !paths.is_empty())
|
|
34
|
+
.unwrap_or_else(|| mentioned_paths(root, prompt.as_ref())),
|
|
23
35
|
))
|
|
24
36
|
}
|
|
25
37
|
|
|
@@ -119,7 +131,7 @@ fn forbidden_context() -> Vec<String> {
|
|
|
119
131
|
.collect()
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
fn mentioned_paths(prompt: &str) -> Vec<String> {
|
|
134
|
+
fn mentioned_paths(root: &Path, prompt: &str) -> Vec<String> {
|
|
123
135
|
prompt
|
|
124
136
|
.split_whitespace()
|
|
125
137
|
.map(|token| {
|
|
@@ -127,8 +139,50 @@ fn mentioned_paths(prompt: &str) -> Vec<String> {
|
|
|
127
139
|
matches!(ch, '"' | '\'' | '`' | ',' | ';' | ':' | ')' | '(')
|
|
128
140
|
})
|
|
129
141
|
})
|
|
130
|
-
.filter(|token| token.contains('/') || is_source_path(token) || token.ends_with(".md"))
|
|
131
142
|
.filter(|token| !token.starts_with('-') && !token.starts_with("http"))
|
|
132
143
|
.map(|token| token.trim_start_matches("./").replace('\\', "/"))
|
|
144
|
+
.filter(|path| path_looks_contextual(path))
|
|
145
|
+
.filter(|path| path_exists_or_has_repo_parent(root, path))
|
|
133
146
|
.collect()
|
|
134
147
|
}
|
|
148
|
+
|
|
149
|
+
fn envelope_referenced_paths(prompt: &str) -> Option<Vec<String>> {
|
|
150
|
+
let input =
|
|
151
|
+
serde_json::from_str::<PromptEnvelopeContext>(prompt_envelope_json(prompt)?).ok()?;
|
|
152
|
+
if input.schema.as_deref() != Some("naome.prompt-envelope.v1") {
|
|
153
|
+
return None;
|
|
154
|
+
}
|
|
155
|
+
Some(
|
|
156
|
+
input
|
|
157
|
+
.referenced_paths
|
|
158
|
+
.unwrap_or_default()
|
|
159
|
+
.into_iter()
|
|
160
|
+
.map(|path| path.trim_start_matches("./").replace('\\', "/"))
|
|
161
|
+
.filter(|path| is_repo_relative_path(path))
|
|
162
|
+
.filter(|path| path_looks_contextual(path))
|
|
163
|
+
.collect(),
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn is_repo_relative_path(path: &str) -> bool {
|
|
168
|
+
let candidate = Path::new(path);
|
|
169
|
+
!path.contains(':')
|
|
170
|
+
&& !candidate.is_absolute()
|
|
171
|
+
&& !candidate
|
|
172
|
+
.components()
|
|
173
|
+
.any(|component| matches!(component, Component::ParentDir))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn path_looks_contextual(path: &str) -> bool {
|
|
177
|
+
path.contains('/') || is_source_path(path) || path.ends_with(".md")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn path_exists_or_has_repo_parent(root: &Path, path: &str) -> bool {
|
|
181
|
+
let candidate = root.join(path);
|
|
182
|
+
if candidate.exists() {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
candidate
|
|
186
|
+
.parent()
|
|
187
|
+
.is_some_and(|parent| parent != root && parent.exists())
|
|
188
|
+
}
|
|
@@ -29,12 +29,10 @@ pub(crate) fn is_integrity_hash(value: &str) -> bool {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
pub(crate) fn native_integrity_from_naome_command(content: &str) -> Option<String> {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
let value = &rest[..end];
|
|
37
|
-
is_integrity_hash(value).then(|| value.to_string())
|
|
32
|
+
content
|
|
33
|
+
.lines()
|
|
34
|
+
.find_map(native_integrity_assignment_value)
|
|
35
|
+
.map(ToString::to_string)
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
fn machine_sha256(relative_path: &str, content: &[u8]) -> String {
|
|
@@ -43,17 +41,22 @@ fn machine_sha256(relative_path: &str, content: &[u8]) -> String {
|
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
fn normalize_machine_owned_content(relative_path: &str, content: &[u8]) -> Vec<u8> {
|
|
44
|
+
let mut normalized = content.to_vec();
|
|
45
|
+
|
|
46
46
|
if relative_path == HEALTH_CHECKER_RELATIVE_PATH
|
|
47
47
|
|| relative_path == TASK_STATE_CHECKER_RELATIVE_PATH
|
|
48
48
|
{
|
|
49
|
-
|
|
49
|
+
normalized = replace_expected_integrity_block(&normalized);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
if relative_path ==
|
|
53
|
-
|
|
52
|
+
if relative_path == HEALTH_CHECKER_RELATIVE_PATH
|
|
53
|
+
|| relative_path == TASK_STATE_CHECKER_RELATIVE_PATH
|
|
54
|
+
|| relative_path == NAOME_COMMAND_RELATIVE_PATH
|
|
55
|
+
{
|
|
56
|
+
normalized = replace_native_integrity_line(&normalized);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
normalized
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
fn replace_expected_integrity_block(content: &[u8]) -> Vec<u8> {
|
|
@@ -79,18 +82,33 @@ fn replace_expected_integrity_block(content: &[u8]) -> Vec<u8> {
|
|
|
79
82
|
|
|
80
83
|
fn replace_native_integrity_line(content: &[u8]) -> Vec<u8> {
|
|
81
84
|
let text = String::from_utf8_lossy(content);
|
|
82
|
-
let
|
|
83
|
-
let Some(start) = text.find(prefix) else {
|
|
84
|
-
return content.to_vec();
|
|
85
|
-
};
|
|
86
|
-
let Some(relative_end) = text[start..].find(";\n") else {
|
|
87
|
-
return content.to_vec();
|
|
88
|
-
};
|
|
89
|
-
let end = start + relative_end + ";\n".len();
|
|
90
|
-
|
|
85
|
+
let mut changed = false;
|
|
91
86
|
let mut next = String::with_capacity(text.len());
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
|
|
88
|
+
for segment in text.split_inclusive('\n') {
|
|
89
|
+
let (line, ending) = segment
|
|
90
|
+
.strip_suffix('\n')
|
|
91
|
+
.map(|line| (line, "\n"))
|
|
92
|
+
.unwrap_or((segment, ""));
|
|
93
|
+
if native_integrity_assignment_value(line).is_some() {
|
|
94
|
+
next.push_str("const expectedNativeBinaryIntegrity = \"sha256:generated\";");
|
|
95
|
+
next.push_str(ending);
|
|
96
|
+
changed = true;
|
|
97
|
+
} else {
|
|
98
|
+
next.push_str(segment);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if changed {
|
|
103
|
+
next.into_bytes()
|
|
104
|
+
} else {
|
|
105
|
+
content.to_vec()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn native_integrity_assignment_value(line: &str) -> Option<&str> {
|
|
110
|
+
let value = line
|
|
111
|
+
.strip_prefix("const expectedNativeBinaryIntegrity = \"")?
|
|
112
|
+
.strip_suffix("\";")?;
|
|
113
|
+
is_integrity_hash(value).then_some(value)
|
|
96
114
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use crate::install_plan::{MACHINE_OWNED_PATHS, PROJECT_OWNED_PATHS};
|
|
6
|
+
|
|
7
|
+
pub(super) fn validate_manifest_shape(manifest: &Value, errors: &mut Vec<String>) {
|
|
8
|
+
let Some(object) = manifest.as_object() else {
|
|
9
|
+
errors.push(".naome/manifest.json must be a JSON object.".to_string());
|
|
10
|
+
return;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if object.get("name").and_then(Value::as_str) != Some("naome") {
|
|
14
|
+
errors.push(".naome/manifest.json name must be naome.".to_string());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if !object
|
|
18
|
+
.get("harnessVersion")
|
|
19
|
+
.and_then(Value::as_str)
|
|
20
|
+
.is_some_and(is_version)
|
|
21
|
+
{
|
|
22
|
+
errors.push(".naome/manifest.json harnessVersion must be semver.".to_string());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if !string_array(object.get("machineOwned")).is_some() {
|
|
26
|
+
errors.push(".naome/manifest.json machineOwned must be a string array.".to_string());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if !string_array(object.get("projectOwned")).is_some() {
|
|
30
|
+
errors.push(".naome/manifest.json projectOwned must be a string array.".to_string());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if !object.get("integrity").is_some_and(Value::is_object) {
|
|
34
|
+
errors.push(".naome/manifest.json integrity must be an object.".to_string());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub(super) fn validate_manifest_ownership(manifest: &Value, errors: &mut Vec<String>) {
|
|
39
|
+
let Some(object) = manifest.as_object() else {
|
|
40
|
+
return;
|
|
41
|
+
};
|
|
42
|
+
let Some(machine_owned) = string_array(object.get("machineOwned")) else {
|
|
43
|
+
return;
|
|
44
|
+
};
|
|
45
|
+
let Some(project_owned) = string_array(object.get("projectOwned")) else {
|
|
46
|
+
return;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
validate_contains_all(
|
|
50
|
+
&machine_owned,
|
|
51
|
+
MACHINE_OWNED_PATHS,
|
|
52
|
+
".naome/manifest.json machineOwned",
|
|
53
|
+
errors,
|
|
54
|
+
);
|
|
55
|
+
validate_contains_all(
|
|
56
|
+
&project_owned,
|
|
57
|
+
PROJECT_OWNED_PATHS,
|
|
58
|
+
".naome/manifest.json projectOwned",
|
|
59
|
+
errors,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub(super) fn string_array(value: Option<&Value>) -> Option<Vec<String>> {
|
|
64
|
+
value.and_then(Value::as_array).and_then(|entries| {
|
|
65
|
+
entries
|
|
66
|
+
.iter()
|
|
67
|
+
.map(|entry| {
|
|
68
|
+
entry
|
|
69
|
+
.as_str()
|
|
70
|
+
.filter(|text| !text.trim().is_empty())
|
|
71
|
+
.map(ToString::to_string)
|
|
72
|
+
})
|
|
73
|
+
.collect()
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn validate_contains_all(
|
|
78
|
+
actual_values: &[String],
|
|
79
|
+
expected_values: &[&str],
|
|
80
|
+
field_name: &str,
|
|
81
|
+
errors: &mut Vec<String>,
|
|
82
|
+
) {
|
|
83
|
+
let actual: HashSet<&str> = actual_values.iter().map(String::as_str).collect();
|
|
84
|
+
for expected in expected_values {
|
|
85
|
+
if !actual.contains(expected) {
|
|
86
|
+
errors.push(format!("{field_name} must include {expected}."));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn is_version(value: &str) -> bool {
|
|
92
|
+
let parts: Vec<&str> = value.split('.').collect();
|
|
93
|
+
parts.len() == 3
|
|
94
|
+
&& parts
|
|
95
|
+
.iter()
|
|
96
|
+
.all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()))
|
|
97
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
mod integrity;
|
|
2
|
+
mod manifest;
|
|
2
3
|
|
|
3
|
-
use std::collections::
|
|
4
|
+
use std::collections::HashMap;
|
|
4
5
|
use std::fs;
|
|
5
6
|
use std::path::{Component, Path};
|
|
6
7
|
|
|
@@ -11,6 +12,7 @@ use self::integrity::{
|
|
|
11
12
|
is_integrity_hash, native_integrity_from_naome_command, sha256_bytes,
|
|
12
13
|
NAOME_COMMAND_RELATIVE_PATH, NATIVE_BINARY_RELATIVE_PATH,
|
|
13
14
|
};
|
|
15
|
+
use self::manifest::{string_array, validate_manifest_ownership, validate_manifest_shape};
|
|
14
16
|
use crate::install_plan::{MACHINE_OWNED_PATHS, PROJECT_OWNED_PATHS};
|
|
15
17
|
use crate::models::NaomeError;
|
|
16
18
|
|
|
@@ -56,62 +58,6 @@ pub fn validate_harness_health(
|
|
|
56
58
|
Ok(errors)
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
fn validate_manifest_shape(manifest: &Value, errors: &mut Vec<String>) {
|
|
60
|
-
let Some(object) = manifest.as_object() else {
|
|
61
|
-
errors.push(".naome/manifest.json must be a JSON object.".to_string());
|
|
62
|
-
return;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
if object.get("name").and_then(Value::as_str) != Some("naome") {
|
|
66
|
-
errors.push(".naome/manifest.json name must be naome.".to_string());
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if !object
|
|
70
|
-
.get("harnessVersion")
|
|
71
|
-
.and_then(Value::as_str)
|
|
72
|
-
.is_some_and(is_version)
|
|
73
|
-
{
|
|
74
|
-
errors.push(".naome/manifest.json harnessVersion must be semver.".to_string());
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if !string_array(object.get("machineOwned")).is_some() {
|
|
78
|
-
errors.push(".naome/manifest.json machineOwned must be a string array.".to_string());
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if !string_array(object.get("projectOwned")).is_some() {
|
|
82
|
-
errors.push(".naome/manifest.json projectOwned must be a string array.".to_string());
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if !object.get("integrity").is_some_and(Value::is_object) {
|
|
86
|
-
errors.push(".naome/manifest.json integrity must be an object.".to_string());
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
fn validate_manifest_ownership(manifest: &Value, errors: &mut Vec<String>) {
|
|
91
|
-
let Some(object) = manifest.as_object() else {
|
|
92
|
-
return;
|
|
93
|
-
};
|
|
94
|
-
let Some(machine_owned) = string_array(object.get("machineOwned")) else {
|
|
95
|
-
return;
|
|
96
|
-
};
|
|
97
|
-
let Some(project_owned) = string_array(object.get("projectOwned")) else {
|
|
98
|
-
return;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
validate_contains_all(
|
|
102
|
-
&machine_owned,
|
|
103
|
-
MACHINE_OWNED_PATHS,
|
|
104
|
-
".naome/manifest.json machineOwned",
|
|
105
|
-
errors,
|
|
106
|
-
);
|
|
107
|
-
validate_contains_all(
|
|
108
|
-
&project_owned,
|
|
109
|
-
PROJECT_OWNED_PATHS,
|
|
110
|
-
".naome/manifest.json projectOwned",
|
|
111
|
-
errors,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
61
|
fn validate_manifest_integrity(
|
|
116
62
|
root: &Path,
|
|
117
63
|
manifest: &Value,
|
|
@@ -186,13 +132,41 @@ fn validate_native_decision_binary(
|
|
|
186
132
|
return Ok(());
|
|
187
133
|
};
|
|
188
134
|
|
|
189
|
-
|
|
135
|
+
let wrapper_paths = [
|
|
136
|
+
NAOME_COMMAND_RELATIVE_PATH,
|
|
137
|
+
".naome/bin/check-harness-health.js",
|
|
138
|
+
".naome/bin/check-task-state.js",
|
|
139
|
+
];
|
|
140
|
+
let wrapper_integrity = wrapper_paths
|
|
141
|
+
.iter()
|
|
142
|
+
.map(|relative_path| {
|
|
143
|
+
native_integrity_from_regular_file(root, relative_path)
|
|
144
|
+
.map(|expected| (*relative_path, expected))
|
|
145
|
+
})
|
|
146
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
147
|
+
let native_is_declared = machine_owned
|
|
148
|
+
.iter()
|
|
149
|
+
.any(|entry| entry == NATIVE_BINARY_RELATIVE_PATH);
|
|
150
|
+
let native_has_integrity = integrity.contains_key(NATIVE_BINARY_RELATIVE_PATH);
|
|
151
|
+
let native_path_present = fs::symlink_metadata(root.join(NATIVE_BINARY_RELATIVE_PATH)).is_ok();
|
|
152
|
+
let wrapper_requires_native = wrapper_integrity
|
|
190
153
|
.iter()
|
|
191
|
-
.any(|
|
|
154
|
+
.any(|(_, expected)| expected.is_some());
|
|
155
|
+
|
|
156
|
+
if !native_is_declared
|
|
157
|
+
&& !native_has_integrity
|
|
158
|
+
&& !wrapper_requires_native
|
|
159
|
+
&& !native_path_present
|
|
192
160
|
{
|
|
193
161
|
return Ok(());
|
|
194
162
|
}
|
|
195
163
|
|
|
164
|
+
if !native_is_declared {
|
|
165
|
+
errors.push(format!(
|
|
166
|
+
".naome/manifest.json machineOwned must include {NATIVE_BINARY_RELATIVE_PATH}."
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
|
|
196
170
|
validate_regular_file(root, NATIVE_BINARY_RELATIVE_PATH, errors)?;
|
|
197
171
|
|
|
198
172
|
if !root.join(NATIVE_BINARY_RELATIVE_PATH).exists()
|
|
@@ -222,22 +196,36 @@ fn validate_native_decision_binary(
|
|
|
222
196
|
));
|
|
223
197
|
}
|
|
224
198
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
let command_expected = native_integrity_from_naome_command(&command_content);
|
|
232
|
-
if command_expected.as_deref() != Some(manifest_expected) {
|
|
233
|
-
errors.push(format!(
|
|
234
|
-
"{NAOME_COMMAND_RELATIVE_PATH} native binary integrity does not match .naome/manifest.json."
|
|
235
|
-
));
|
|
199
|
+
for (relative_path, expected) in wrapper_integrity {
|
|
200
|
+
if expected.as_deref() != Some(manifest_expected) {
|
|
201
|
+
errors.push(format!(
|
|
202
|
+
"{relative_path} native binary integrity does not match .naome/manifest.json."
|
|
203
|
+
));
|
|
204
|
+
}
|
|
236
205
|
}
|
|
237
206
|
|
|
238
207
|
Ok(())
|
|
239
208
|
}
|
|
240
209
|
|
|
210
|
+
fn native_integrity_from_regular_file(
|
|
211
|
+
root: &Path,
|
|
212
|
+
relative_path: &str,
|
|
213
|
+
) -> Result<Option<String>, NaomeError> {
|
|
214
|
+
let file_path = root.join(relative_path);
|
|
215
|
+
if has_symlink_in_path(root, relative_path)? {
|
|
216
|
+
return Ok(None);
|
|
217
|
+
}
|
|
218
|
+
let Ok(metadata) = fs::symlink_metadata(&file_path) else {
|
|
219
|
+
return Ok(None);
|
|
220
|
+
};
|
|
221
|
+
if !metadata.is_file() {
|
|
222
|
+
return Ok(None);
|
|
223
|
+
}
|
|
224
|
+
Ok(native_integrity_from_naome_command(&fs::read_to_string(
|
|
225
|
+
file_path,
|
|
226
|
+
)?))
|
|
227
|
+
}
|
|
228
|
+
|
|
241
229
|
fn validate_naome_ignore(root: &Path, errors: &mut Vec<String>) -> Result<(), NaomeError> {
|
|
242
230
|
let relative_path = ".naomeignore";
|
|
243
231
|
if !root.join(relative_path).exists() || has_symlink_in_path(root, relative_path)? {
|
|
@@ -342,20 +330,6 @@ fn read_json(
|
|
|
342
330
|
}
|
|
343
331
|
}
|
|
344
332
|
|
|
345
|
-
fn validate_contains_all(
|
|
346
|
-
actual_values: &[String],
|
|
347
|
-
expected_values: &[&str],
|
|
348
|
-
field_name: &str,
|
|
349
|
-
errors: &mut Vec<String>,
|
|
350
|
-
) {
|
|
351
|
-
let actual: HashSet<&str> = actual_values.iter().map(String::as_str).collect();
|
|
352
|
-
for expected in expected_values {
|
|
353
|
-
if !actual.contains(expected) {
|
|
354
|
-
errors.push(format!("{field_name} must include {expected}."));
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
333
|
fn validate_regular_file(
|
|
360
334
|
root: &Path,
|
|
361
335
|
relative_path: &str,
|
|
@@ -423,25 +397,3 @@ fn has_symlink_in_path(root: &Path, relative_path: &str) -> Result<bool, NaomeEr
|
|
|
423
397
|
|
|
424
398
|
Ok(false)
|
|
425
399
|
}
|
|
426
|
-
|
|
427
|
-
fn string_array(value: Option<&Value>) -> Option<Vec<String>> {
|
|
428
|
-
value.and_then(Value::as_array).and_then(|entries| {
|
|
429
|
-
entries
|
|
430
|
-
.iter()
|
|
431
|
-
.map(|entry| {
|
|
432
|
-
entry
|
|
433
|
-
.as_str()
|
|
434
|
-
.filter(|text| !text.trim().is_empty())
|
|
435
|
-
.map(ToString::to_string)
|
|
436
|
-
})
|
|
437
|
-
.collect()
|
|
438
|
-
})
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
fn is_version(value: &str) -> bool {
|
|
442
|
-
let parts: Vec<&str> = value.split('.').collect();
|
|
443
|
-
parts.len() == 3
|
|
444
|
-
&& parts
|
|
445
|
-
.iter()
|
|
446
|
-
.all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()))
|
|
447
|
-
}
|