@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
|
@@ -4,8 +4,8 @@ use std::fs;
|
|
|
4
4
|
|
|
5
5
|
use naome_core::{
|
|
6
6
|
check_repository_quality, check_repository_quality_paths, check_semantic_legacy,
|
|
7
|
-
init_repository_quality, init_repository_quality_with_mode,
|
|
8
|
-
QualityInitMode, QualityMode,
|
|
7
|
+
clear_quality_cache, init_repository_quality, init_repository_quality_with_mode,
|
|
8
|
+
quality_cache_status, QualityInitMode, QualityMode,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
use repo_support::TestRepo;
|
|
@@ -140,6 +140,67 @@ fn second_report_uses_file_analysis_cache() {
|
|
|
140
140
|
assert_eq!(second.summary.cache_misses, 0);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
#[cfg(unix)]
|
|
144
|
+
#[test]
|
|
145
|
+
fn report_skips_repository_symlinks_without_caching_target_contents() {
|
|
146
|
+
let repo = quality_repo("quality-cache-skips-file-symlink");
|
|
147
|
+
let victim = repo.path().join("../naome-victim-secret.txt");
|
|
148
|
+
fs::write(&victim, "VALIDATION_SECRET_raw_lines_12345\n").unwrap();
|
|
149
|
+
std::os::unix::fs::symlink(&victim, repo.path().join("leak.txt")).unwrap();
|
|
150
|
+
repo.git(&["add", "leak.txt"]);
|
|
151
|
+
repo.git(&["commit", "-m", "tracked symlink"]);
|
|
152
|
+
|
|
153
|
+
let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
|
|
154
|
+
let cache = quality_cache_status(repo.path()).unwrap();
|
|
155
|
+
|
|
156
|
+
assert!(!report.scanned_paths.contains(&"leak.txt".to_string()));
|
|
157
|
+
assert_eq!(cache.entry_count, 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[cfg(unix)]
|
|
161
|
+
#[test]
|
|
162
|
+
fn path_budget_skips_symlinks_before_reading_target_metadata() {
|
|
163
|
+
let repo = quality_repo("quality-budget-skips-symlink");
|
|
164
|
+
let victim = repo.path().join("../naome-large-victim.txt");
|
|
165
|
+
fs::write(&victim, "x".repeat(1024 * 1024 + 1)).unwrap();
|
|
166
|
+
std::os::unix::fs::symlink(&victim, repo.path().join("large-link.txt")).unwrap();
|
|
167
|
+
repo.git(&["add", "large-link.txt"]);
|
|
168
|
+
repo.git(&["commit", "-m", "tracked large symlink"]);
|
|
169
|
+
|
|
170
|
+
let report = check_repository_quality_paths(repo.path(), &["large-link.txt"]).unwrap();
|
|
171
|
+
|
|
172
|
+
assert!(!report.scanned_paths.contains(&"large-link.txt".to_string()));
|
|
173
|
+
assert!(!report
|
|
174
|
+
.summary
|
|
175
|
+
.reason_codes
|
|
176
|
+
.contains(&"max_file_bytes".to_string()));
|
|
177
|
+
fs::remove_file(victim).unwrap();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[cfg(unix)]
|
|
181
|
+
#[test]
|
|
182
|
+
fn cache_operations_reject_symlinked_cache_path_components() {
|
|
183
|
+
let repo = quality_repo("quality-cache-rejects-cache-symlink");
|
|
184
|
+
repo.write_file("src/a.js", "export const value = 1;\n");
|
|
185
|
+
repo.commit_all("baseline");
|
|
186
|
+
let outside = repo.path().join("../naome-outside-cache");
|
|
187
|
+
fs::create_dir_all(outside.join("quality")).unwrap();
|
|
188
|
+
fs::remove_dir_all(repo.path().join(".naome/cache")).ok();
|
|
189
|
+
std::os::unix::fs::symlink(&outside, repo.path().join(".naome/cache")).unwrap();
|
|
190
|
+
|
|
191
|
+
let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
|
|
192
|
+
let clear_error = clear_quality_cache(repo.path()).unwrap_err();
|
|
193
|
+
|
|
194
|
+
assert_eq!(report.summary.cache_hits, 0);
|
|
195
|
+
assert!(outside.join("quality").is_dir());
|
|
196
|
+
assert!(!fs::read_dir(outside.join("quality"))
|
|
197
|
+
.unwrap()
|
|
198
|
+
.any(|entry| entry.is_ok()));
|
|
199
|
+
assert!(clear_error
|
|
200
|
+
.to_string()
|
|
201
|
+
.contains("must not contain symlinks"));
|
|
202
|
+
}
|
|
203
|
+
|
|
143
204
|
#[test]
|
|
144
205
|
fn report_budget_marks_truncated_reports() {
|
|
145
206
|
let repo = quality_repo("quality-report-budget");
|
|
@@ -6,10 +6,16 @@ use super::TestRepo;
|
|
|
6
6
|
|
|
7
7
|
const NEW_README_TASK_PROMPT: &str = "Add another line to README as a new task.";
|
|
8
8
|
|
|
9
|
+
pub fn prompt_env(prompt: &str, workflow: &str, task: &str, mutation_intent: &str) -> String {
|
|
10
|
+
format!(
|
|
11
|
+
"```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"{mutation_intent}\",\"workflowAction\":\"{workflow}\",\"taskIntent\":\"{task}\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
pub fn route_commit_request(repo: &TestRepo) -> RouteDecision {
|
|
10
16
|
evaluate_route(
|
|
11
17
|
repo.path(),
|
|
12
|
-
"commit my changes",
|
|
18
|
+
&prompt_env("commit my changes", "commit_request", "none", "commit"),
|
|
13
19
|
RouteOptions {
|
|
14
20
|
execute: true,
|
|
15
21
|
evaluation: EvaluationOptions::offline(),
|
|
@@ -29,7 +35,7 @@ pub fn try_route_new_task(
|
|
|
29
35
|
) -> Result<RouteDecision, NaomeError> {
|
|
30
36
|
evaluate_route(
|
|
31
37
|
repo.path(),
|
|
32
|
-
prompt,
|
|
38
|
+
&prompt_env(prompt, "none", "new_task", "modify_files"),
|
|
33
39
|
RouteOptions {
|
|
34
40
|
execute,
|
|
35
41
|
evaluation: EvaluationOptions::offline(),
|
|
@@ -3,11 +3,40 @@ use std::fs;
|
|
|
3
3
|
|
|
4
4
|
mod repo_support;
|
|
5
5
|
|
|
6
|
+
use naome_core::{evaluate_route, EvaluationOptions, RouteOptions};
|
|
6
7
|
use repo_support::{
|
|
7
8
|
assert_commit_paths, assert_isolated_worktree_ready, route_new_task, route_readme_task,
|
|
8
9
|
TestRepo,
|
|
9
10
|
};
|
|
10
11
|
|
|
12
|
+
#[test]
|
|
13
|
+
fn execute_route_with_raw_prompt_requests_normalization_without_mutating() {
|
|
14
|
+
let repo = TestRepo::new("route-raw-prompt-normalize-first");
|
|
15
|
+
repo.init_git();
|
|
16
|
+
repo.write_file("README.md", "# Baseline\n");
|
|
17
|
+
repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
|
|
18
|
+
repo.git(&["add", "."]);
|
|
19
|
+
repo.git(&["commit", "-m", "baseline"]);
|
|
20
|
+
let before = repo.git_stdout(&["rev-parse", "HEAD"]);
|
|
21
|
+
|
|
22
|
+
let route = evaluate_route(
|
|
23
|
+
repo.path(),
|
|
24
|
+
"Please implement, commit, push, and create the MR.",
|
|
25
|
+
RouteOptions {
|
|
26
|
+
execute: true,
|
|
27
|
+
evaluation: EvaluationOptions::offline(),
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
.unwrap();
|
|
31
|
+
|
|
32
|
+
assert_eq!(route.prompt_intent, "prompt_normalization_required");
|
|
33
|
+
assert_eq!(route.policy_action, "normalize_prompt_first");
|
|
34
|
+
assert!(!route.mutation_performed);
|
|
35
|
+
assert!(!route.can_create_task);
|
|
36
|
+
assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
|
|
37
|
+
assert!(repo.git_status_short().is_empty());
|
|
38
|
+
}
|
|
39
|
+
|
|
11
40
|
#[test]
|
|
12
41
|
fn dry_route_reports_auto_baseline_without_mutating() {
|
|
13
42
|
let repo = TestRepo::completed_task_with_diff("route-dry-auto");
|
|
@@ -7,6 +7,12 @@ mod repo_support;
|
|
|
7
7
|
|
|
8
8
|
use repo_support::TestRepo;
|
|
9
9
|
|
|
10
|
+
fn route_prompt(prompt: &str, workflow: &str, task: &str, mutation_intent: &str) -> String {
|
|
11
|
+
format!(
|
|
12
|
+
"```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"{mutation_intent}\",\"workflowAction\":\"{workflow}\",\"taskIntent\":\"{task}\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
#[test]
|
|
11
17
|
fn execute_route_does_not_mutate_when_prompt_blocks_commit() {
|
|
12
18
|
let repo = TestRepo::completed_task_with_diff("route-no-commit");
|
|
@@ -14,7 +20,12 @@ fn execute_route_does_not_mutate_when_prompt_blocks_commit() {
|
|
|
14
20
|
|
|
15
21
|
let route = evaluate_route(
|
|
16
22
|
repo.path(),
|
|
17
|
-
|
|
23
|
+
&route_prompt(
|
|
24
|
+
"Do not commit. Start a new task after this.",
|
|
25
|
+
"no_commit_request",
|
|
26
|
+
"new_task",
|
|
27
|
+
"none",
|
|
28
|
+
),
|
|
18
29
|
RouteOptions {
|
|
19
30
|
execute: true,
|
|
20
31
|
evaluation: EvaluationOptions::offline(),
|
|
@@ -41,7 +52,7 @@ fn explicit_route_commit_baseline_leaves_unrelated_user_edit_unstaged() {
|
|
|
41
52
|
|
|
42
53
|
let route = evaluate_route(
|
|
43
54
|
repo.path(),
|
|
44
|
-
"commit_task_baseline",
|
|
55
|
+
&route_prompt("commit_task_baseline", "commit_request", "none", "commit"),
|
|
45
56
|
RouteOptions {
|
|
46
57
|
execute: true,
|
|
47
58
|
evaluation: EvaluationOptions::offline(),
|
|
@@ -67,7 +78,12 @@ fn execute_route_journals_external_commit_after_completed_task() {
|
|
|
67
78
|
|
|
68
79
|
let route = evaluate_route(
|
|
69
80
|
repo.path(),
|
|
70
|
-
|
|
81
|
+
&route_prompt(
|
|
82
|
+
"Create a new task for README polish.",
|
|
83
|
+
"none",
|
|
84
|
+
"new_task",
|
|
85
|
+
"modify_files",
|
|
86
|
+
),
|
|
71
87
|
RouteOptions {
|
|
72
88
|
execute: true,
|
|
73
89
|
evaluation: EvaluationOptions::offline(),
|
|
@@ -94,7 +110,12 @@ fn explain_reports_winning_rule_and_mutation_plan_without_executing() {
|
|
|
94
110
|
|
|
95
111
|
let explain = explain_route(
|
|
96
112
|
repo.path(),
|
|
97
|
-
|
|
113
|
+
&route_prompt(
|
|
114
|
+
"Start a new task for README polish.",
|
|
115
|
+
"none",
|
|
116
|
+
"new_task",
|
|
117
|
+
"modify_files",
|
|
118
|
+
),
|
|
98
119
|
EvaluationOptions::offline(),
|
|
99
120
|
)
|
|
100
121
|
.unwrap();
|
|
@@ -126,7 +147,7 @@ fn unhealthy_harness_route_blocks_normal_work() {
|
|
|
126
147
|
|
|
127
148
|
let route = evaluate_route(
|
|
128
149
|
repo.path(),
|
|
129
|
-
"Create a new task.",
|
|
150
|
+
&route_prompt("Create a new task.", "none", "new_task", "modify_files"),
|
|
130
151
|
RouteOptions {
|
|
131
152
|
execute: true,
|
|
132
153
|
evaluation: EvaluationOptions::online(),
|
|
@@ -9,6 +9,12 @@ use repo_support::{
|
|
|
9
9
|
TestRepo,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
fn repair_prompt(prompt: &str) -> String {
|
|
13
|
+
format!(
|
|
14
|
+
"```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"fix\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"repair_request\",\"taskIntent\":\"none\",\"risk\":\"none\",\"requestedActions\":[\"repair\"],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
#[test]
|
|
13
19
|
fn execute_route_baselines_harness_refresh_before_dirty_repo_worktree() {
|
|
14
20
|
let repo = TestRepo::dirty_harness_refresh_repo("route-dirty-harness-refresh-worktree", true);
|
|
@@ -66,7 +72,7 @@ fn execute_route_repair_request_baselines_harness_refresh_only() {
|
|
|
66
72
|
|
|
67
73
|
let route = evaluate_route(
|
|
68
74
|
repo.path(),
|
|
69
|
-
"please repair all",
|
|
75
|
+
&repair_prompt("please repair all"),
|
|
70
76
|
RouteOptions {
|
|
71
77
|
execute: true,
|
|
72
78
|
evaluation: EvaluationOptions::offline(),
|
|
@@ -27,7 +27,7 @@ fn execute_route_does_not_mutate_or_offer_clear_commit_for_dirty_unowned_diff()
|
|
|
27
27
|
)
|
|
28
28
|
.unwrap();
|
|
29
29
|
|
|
30
|
-
assert_eq!(route.policy_action, "
|
|
30
|
+
assert_eq!(route.policy_action, "normalize_prompt_first");
|
|
31
31
|
assert!(!route.mutation_performed);
|
|
32
32
|
assert!(!route.can_create_task);
|
|
33
33
|
assert_eq!(route.executed_actions, Vec::<String>::new());
|
|
@@ -6,6 +6,12 @@ use task_state_compact_support::{
|
|
|
6
6
|
compact_state, large_compact_state, large_expanded_state, legacy_state, MiniRepo,
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
fn new_task_prompt(prompt: &str) -> String {
|
|
10
|
+
format!(
|
|
11
|
+
"```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
#[test]
|
|
10
16
|
fn legacy_v1_proof_results_remain_valid() {
|
|
11
17
|
let repo = MiniRepo::new();
|
|
@@ -30,7 +36,7 @@ fn compact_path_sets_and_batches_cover_completion_commit_and_route() {
|
|
|
30
36
|
|
|
31
37
|
let route = evaluate_route(
|
|
32
38
|
repo.path(),
|
|
33
|
-
"new task",
|
|
39
|
+
&new_task_prompt("new task"),
|
|
34
40
|
RouteOptions {
|
|
35
41
|
execute: false,
|
|
36
42
|
evaluation: EvaluationOptions::offline(),
|
package/installer/filesystem.js
CHANGED
|
@@ -64,6 +64,44 @@ export function archiveUpgradePath(ctx, archiveDirName, relativePath) {
|
|
|
64
64
|
return join(ctx.targetRoot, ".naome", "archive", archiveDirName, relativePath);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export function assertWritableArchivePath(ctx, archiveDirName, relativePath) {
|
|
68
|
+
const archiveRelativePath = join(".naome", "archive", archiveDirName, relativePath);
|
|
69
|
+
const parts = archiveRelativePath.split(/[\\/]+/);
|
|
70
|
+
let current = ctx.targetRoot;
|
|
71
|
+
|
|
72
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
73
|
+
const part = parts[index];
|
|
74
|
+
const isLeaf = index === parts.length - 1;
|
|
75
|
+
current = join(current, part);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const stats = lstatSync(current);
|
|
79
|
+
if (stats.isSymbolicLink()) {
|
|
80
|
+
printError(ctx, `NAOME cannot archive ${relativePath} safely.`);
|
|
81
|
+
console.error(`${archiveRelativePath} must not contain symbolic links.`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!isLeaf && !stats.isDirectory()) {
|
|
86
|
+
printError(ctx, `NAOME cannot archive ${relativePath} because the archive path is not a directory.`);
|
|
87
|
+
console.error(`${join(...parts.slice(0, index + 1))} must be a regular directory or must not exist.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isLeaf && existsSync(current) && !stats.isFile()) {
|
|
92
|
+
printError(ctx, `NAOME cannot archive ${relativePath} because the archive path is not a file.`);
|
|
93
|
+
console.error(`${archiveRelativePath} must be a regular file or must not exist.`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code === "ENOENT") {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
67
105
|
export function ensureArchiveDirectory(ctx) {
|
|
68
106
|
const archivePath = ".naome/archive";
|
|
69
107
|
const targetPath = join(ctx.targetRoot, archivePath);
|
package/installer/flows.js
CHANGED
|
@@ -31,6 +31,8 @@ const legacyOptionalHookPaths = [
|
|
|
31
31
|
"docs/naome/codex-hooks.md",
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
+
const trustedRetiredMachineOwnedPaths = new Set(legacyOptionalHookPaths);
|
|
35
|
+
|
|
34
36
|
export async function runFreshInstall(ctx) {
|
|
35
37
|
await confirmAgentsTakeover(ctx);
|
|
36
38
|
|
|
@@ -110,8 +112,11 @@ function removeRetiredMachineOwnedFiles(ctx, manifest, archiveDirName, options =
|
|
|
110
112
|
...ctx.localOnlyMachineOwnedPaths,
|
|
111
113
|
ctx.nativeBinaryRelativePath,
|
|
112
114
|
].filter(Boolean));
|
|
115
|
+
const manifestRetiredPaths = Array.isArray(manifest?.machineOwned)
|
|
116
|
+
? manifest.machineOwned.filter((path) => trustedRetiredMachineOwnedPaths.has(path))
|
|
117
|
+
: [];
|
|
113
118
|
const retiredPaths = [
|
|
114
|
-
...
|
|
119
|
+
...manifestRetiredPaths,
|
|
115
120
|
...(Array.isArray(options.extraPaths) ? options.extraPaths : []),
|
|
116
121
|
];
|
|
117
122
|
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
} from "node:fs";
|
|
10
10
|
import { dirname, join, relative } from "node:path";
|
|
11
11
|
|
|
12
|
-
import { archiveUpgradePath, hasSymlinkInTargetPath } from "./filesystem.js";
|
|
13
|
-
import {
|
|
12
|
+
import { archiveUpgradePath, assertWritableArchivePath, hasSymlinkInTargetPath } from "./filesystem.js";
|
|
13
|
+
import { machineFileHash } from "./native.js";
|
|
14
14
|
import { printError } from "./output.js";
|
|
15
15
|
|
|
16
16
|
export function ensureTemplateFile(ctx, relativePath) {
|
|
@@ -55,8 +55,13 @@ export function removeLegacyHarnessFile(ctx, relativePath, archiveDirName) {
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const archivePath =
|
|
58
|
+
const archivePath = safeArchivePath(ctx, archiveDirName, relativePath);
|
|
59
|
+
if (archivePath === null) {
|
|
60
|
+
failUnsafeArchivePath(ctx, archiveDirName, relativePath);
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
mkdirSync(dirname(archivePath), { recursive: true });
|
|
64
|
+
assertWritableArchivePath(ctx, archiveDirName, relativePath);
|
|
60
65
|
copyFileSync(targetPath, archivePath);
|
|
61
66
|
unlinkSync(targetPath);
|
|
62
67
|
ctx.updated.push(relativePath);
|
|
@@ -128,17 +133,40 @@ function replaceChangedHarnessFile(ctx, relativePath, archiveDirName, sourcePath
|
|
|
128
133
|
return;
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
const archivePath =
|
|
136
|
+
const archivePath = safeArchivePath(ctx, archiveDirName, relativePath);
|
|
137
|
+
if (archivePath === null) {
|
|
138
|
+
failUnsafeArchivePath(ctx, archiveDirName, relativePath);
|
|
139
|
+
}
|
|
140
|
+
|
|
132
141
|
mkdirSync(dirname(archivePath), { recursive: true });
|
|
142
|
+
assertWritableArchivePath(ctx, archiveDirName, relativePath);
|
|
133
143
|
copyFileSync(targetPath, archivePath);
|
|
134
144
|
writeFileSync(targetPath, nextContent);
|
|
135
145
|
ctx.updated.push(relativePath);
|
|
136
146
|
ctx.archived.push({ from: relativePath, to: relative(ctx.targetRoot, archivePath) });
|
|
137
147
|
}
|
|
138
148
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
149
|
+
function safeArchivePath(ctx, archiveDirName, relativePath) {
|
|
150
|
+
const archivePath = archiveUpgradePath(ctx, archiveDirName, relativePath);
|
|
151
|
+
const archiveParent = relative(ctx.targetRoot, dirname(archivePath));
|
|
152
|
+
if (hasSymlinkInTargetPath(ctx, archiveParent)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (existsSync(archivePath)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return archivePath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function failUnsafeArchivePath(ctx, archiveDirName, relativePath) {
|
|
163
|
+
printError(ctx, `NAOME cannot archive ${relativePath} safely.`);
|
|
164
|
+
console.error(
|
|
165
|
+
`${relative(ctx.targetRoot, archiveUpgradePath(ctx, archiveDirName, relativePath))} must not contain symlinks or pre-existing files.`
|
|
143
166
|
);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function unchangedMachineHash(ctx, relativePath, currentContent, nextContent) {
|
|
171
|
+
return machineFileHash(ctx, relativePath, currentContent) === machineFileHash(ctx, relativePath, nextContent);
|
|
144
172
|
}
|
|
@@ -2,7 +2,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "n
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { hasSymlinkInTargetPath } from "./filesystem.js";
|
|
5
|
-
import {
|
|
5
|
+
import { installedMachineOwnedIntegrity, installedNativeBinaryHash, usesSourceNativeFallback } from "./native.js";
|
|
6
6
|
import { printError } from "./output.js";
|
|
7
7
|
import { isVersion } from "./version.js";
|
|
8
8
|
|
|
@@ -118,7 +118,7 @@ function applyManifestHealthMetadata(ctx, manifest) {
|
|
|
118
118
|
manifest.harnessVersion = ctx.packageVersion;
|
|
119
119
|
manifest.machineOwned = [...ctx.machineOwnedPaths];
|
|
120
120
|
manifest.projectOwned = ctx.projectOwnedPaths;
|
|
121
|
-
manifest.integrity =
|
|
121
|
+
manifest.integrity = installedMachineOwnedIntegrity(ctx);
|
|
122
122
|
|
|
123
123
|
const nativeHash = installedNativeBinaryHash(ctx);
|
|
124
124
|
if (!usesSourceNativeFallback(ctx) && nativeHash) {
|
package/installer/native.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
lstatSync,
|
|
7
7
|
mkdirSync,
|
|
8
8
|
readFileSync,
|
|
9
|
+
unlinkSync,
|
|
9
10
|
writeFileSync,
|
|
10
11
|
} from "node:fs";
|
|
11
12
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -32,8 +33,8 @@ export function installNativeDecisionBinary(ctx) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
if (usesSourceNativeFallback(ctx)) {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
removeInstalledNativeDecisionBinary(ctx);
|
|
37
|
+
patchInstalledNativeIntegrity(ctx, "sha256:generated");
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -52,7 +53,7 @@ export function installNativeDecisionBinary(ctx) {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
chmodSync(targetPath, 0o755);
|
|
55
|
-
|
|
56
|
+
patchInstalledNativeIntegrity(ctx, `sha256:${sourceHash}`);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export function findNativeDecisionBinary(ctx) {
|
|
@@ -75,7 +76,7 @@ export function findNativeDecisionBinary(ctx) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
export function patchInstalledMachineOwnedIntegrity(ctx) {
|
|
78
|
-
const integrityBlock = formatExpectedIntegrityBlock(
|
|
79
|
+
const integrityBlock = formatExpectedIntegrityBlock(installedMachineOwnedIntegrity(ctx));
|
|
79
80
|
|
|
80
81
|
for (const relativePath of [ctx.healthCheckerRelativePath, ctx.taskStateCheckerRelativePath]) {
|
|
81
82
|
const targetPath = join(ctx.targetRoot, relativePath);
|
|
@@ -110,6 +111,23 @@ export function installedNativeBinaryHash(ctx) {
|
|
|
110
111
|
return sha256(readFileSync(targetPath));
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
function removeInstalledNativeDecisionBinary(ctx) {
|
|
115
|
+
const targetPath = join(ctx.targetRoot, ctx.nativeBinaryRelativePath);
|
|
116
|
+
if (!existsSync(targetPath)) {
|
|
117
|
+
ctx.skipped.push(ctx.nativeBinaryRelativePath);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (hasSymlinkInTargetPath(ctx, ctx.nativeBinaryRelativePath) || !lstatSync(targetPath).isFile()) {
|
|
122
|
+
ctx.skipped.push(ctx.nativeBinaryRelativePath);
|
|
123
|
+
ctx.unsafeSkipped.push(ctx.nativeBinaryRelativePath);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
unlinkSync(targetPath);
|
|
128
|
+
ctx.updated.push(ctx.nativeBinaryRelativePath);
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
export function templateIntegrity(ctx) {
|
|
114
132
|
const integrity = {};
|
|
115
133
|
|
|
@@ -121,6 +139,17 @@ export function templateIntegrity(ctx) {
|
|
|
121
139
|
return integrity;
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
export function installedMachineOwnedIntegrity(ctx) {
|
|
143
|
+
const integrity = templateIntegrity(ctx);
|
|
144
|
+
const nativeHash = installedNativeBinaryHash(ctx);
|
|
145
|
+
|
|
146
|
+
if (!usesSourceNativeFallback(ctx) && nativeHash) {
|
|
147
|
+
integrity[ctx.nativeBinaryRelativePath] = `sha256:${nativeHash}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return integrity;
|
|
151
|
+
}
|
|
152
|
+
|
|
124
153
|
export function sha256(content) {
|
|
125
154
|
return createHash("sha256").update(content).digest("hex");
|
|
126
155
|
}
|
|
@@ -132,7 +161,7 @@ export function machineFileHash(ctx, relativePath, content) {
|
|
|
132
161
|
normalized = normalized.toString("utf8").replace(ctx.integrityBlockPattern, ctx.normalizedIntegrityBlock);
|
|
133
162
|
}
|
|
134
163
|
|
|
135
|
-
if (relativePath
|
|
164
|
+
if (hasGeneratedNativeIntegrity(ctx, relativePath)) {
|
|
136
165
|
normalized = normalized.toString("utf8").replace(ctx.nativeIntegrityPattern, ctx.normalizedNativeIntegrity);
|
|
137
166
|
}
|
|
138
167
|
|
|
@@ -143,20 +172,36 @@ export function hasGeneratedIntegrity(ctx, relativePath) {
|
|
|
143
172
|
return relativePath === ctx.healthCheckerRelativePath || relativePath === ctx.taskStateCheckerRelativePath;
|
|
144
173
|
}
|
|
145
174
|
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
export function hasGeneratedNativeIntegrity(ctx, relativePath) {
|
|
176
|
+
return [
|
|
177
|
+
ctx.healthCheckerRelativePath,
|
|
178
|
+
ctx.taskStateCheckerRelativePath,
|
|
179
|
+
ctx.naomeCommandRelativePath,
|
|
180
|
+
].includes(relativePath);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function patchInstalledNativeIntegrity(ctx, expectedIntegrity) {
|
|
184
|
+
const nativeIntegrityPaths = [
|
|
185
|
+
ctx.healthCheckerRelativePath,
|
|
186
|
+
ctx.taskStateCheckerRelativePath,
|
|
187
|
+
ctx.naomeCommandRelativePath,
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
for (const relativePath of nativeIntegrityPaths) {
|
|
191
|
+
const targetPath = join(ctx.targetRoot, relativePath);
|
|
192
|
+
if (!existsSync(targetPath) || hasSymlinkInTargetPath(ctx, relativePath)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
151
195
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
196
|
+
const content = readFileSync(targetPath, "utf8");
|
|
197
|
+
const nextContent = content.replace(
|
|
198
|
+
ctx.nativeIntegrityPattern,
|
|
199
|
+
`const expectedNativeBinaryIntegrity = "${expectedIntegrity}";\n`,
|
|
200
|
+
);
|
|
157
201
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
202
|
+
if (nextContent !== content) {
|
|
203
|
+
writeFileSync(targetPath, nextContent);
|
|
204
|
+
ctx.updated.push(relativePath);
|
|
205
|
+
}
|
|
161
206
|
}
|
|
162
207
|
}
|
|
Binary file
|
package/native/linux-x64/naome
CHANGED
|
Binary file
|