Skip to content

Commit f7d157c

Browse files
committed
fix(tools): enforce rss file tool lifecycle
Address Task 0C review findings with native execute() parity: validate path policy before prepare, honor search wall-time via a generic monotonic clock, publish at most one allowlisted result artifact on Read, and cover cancel/deadline/TOCTOU/replay/quota.
1 parent ccca8e1 commit f7d157c

7 files changed

Lines changed: 1075 additions & 245 deletions

File tree

rss/tools/read_file.rss

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ fn shrink_result(result: map, max_output_bytes: int, token: string) -> map {
324324
let data: map = types::map_map(current, "data");
325325
if content.length > 0 {
326326
let payload: bytes = bytes::from_utf8(content);
327-
let put: map = cap::artifact_put(token, payload, { purpose: "result" });
327+
let put: map = cap::artifact_put_result(token, payload, {});
328328
if map_bool(put, "ok", false) {
329329
let id: string = types::map_string(put, "id", "");
330330
let len: int = map_int(put, "len", content.length);
@@ -363,19 +363,21 @@ pub fn descriptor() -> map {
363363
pub fn validate(arguments: map) -> map {
364364
let mut result: map = { ok: true, code: "", message: "" };
365365
if arguments.has("path") == false {
366-
result = { ok: false, code: "invalid_arguments", message: "path is required" };
366+
result = { ok: false, code: "invalid_arguments", message: "read_file requires path" };
367367
} else {
368368
if type(arguments.path) != "string" {
369-
result = { ok: false, code: "invalid_arguments", message: "path must be a string" };
369+
result = { ok: false, code: "invalid_arguments", message: "read_file requires path" };
370+
} else {
371+
result = validate_tool_path(arguments.path, false);
370372
}
371373
}
372374
if map_bool(result, "ok", false) {
373375
if arguments.has("offset") {
374376
if type(arguments.offset) != "int" {
375-
result = { ok: false, code: "invalid_arguments", message: "offset must be an integer" };
377+
result = { ok: false, code: "invalid_arguments", message: "numeric argument is invalid" };
376378
} else {
377379
if arguments.offset < 0 {
378-
result = { ok: false, code: "invalid_arguments", message: "offset must be an integer" };
380+
result = { ok: false, code: "invalid_arguments", message: "numeric argument is invalid" };
379381
} else {
380382
if arguments.offset == 0 {
381383
result = { ok: false, code: "invalid_offset", message: "read_file offset is 1-based" };
@@ -387,10 +389,10 @@ pub fn validate(arguments: map) -> map {
387389
if map_bool(result, "ok", false) {
388390
if arguments.has("limit") {
389391
if type(arguments.limit) != "int" {
390-
result = { ok: false, code: "invalid_arguments", message: "limit must be an integer" };
392+
result = { ok: false, code: "invalid_arguments", message: "numeric argument is invalid" };
391393
} else {
392394
if arguments.limit < 0 {
393-
result = { ok: false, code: "invalid_arguments", message: "limit must be an integer" };
395+
result = { ok: false, code: "invalid_arguments", message: "numeric argument is invalid" };
394396
}
395397
}
396398
}

rss/tools/search_files.rss

Lines changed: 77 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use json;
55
fn map_int(value: map, key: string, fallback: int) -> int {
66
let mut result: int = fallback;
77
if value.has(key) {
8-
result = value[key];
8+
if type(value[key]) == "int" {
9+
result = value[key];
10+
}
911
}
1012
result
1113
}
@@ -534,6 +536,24 @@ fn push_match(state: map, line: string) -> map {
534536
}
535537

536538
fn observe_limits(state: map) -> map {
539+
let tick: map = cap::clock_monotonic_ms(types::map_string(state, "token", ""));
540+
if map_bool(tick, "ok", false) == false {
541+
state.fatal = true;
542+
state.fatal_code = types::map_string(tick, "code", "cancelled");
543+
state.fatal_message = types::map_string(tick, "message", "clock failed");
544+
state.stop = true;
545+
} else {
546+
let now: int = map_int(tick, "ms", 0);
547+
let start: int = map_int(state, "start_ms", 0);
548+
let mut elapsed: int = now - start;
549+
if elapsed < 0 {
550+
elapsed = 0;
551+
}
552+
if elapsed >= map_int(state, "max_search_wall_time_ms", 2000) {
553+
state.truncated = true;
554+
state.stop = true;
555+
}
556+
}
537557
if map_int(state, "files_visited", 0) >= map_int(state, "max_search_files", 10000) {
538558
state.truncated = true;
539559
state.stop = true;
@@ -675,7 +695,7 @@ fn shrink_result(result: map, max_output_bytes: int, token: string) -> map {
675695
let data: map = types::map_map(current, "data");
676696
if content.length > 0 {
677697
let payload: bytes = bytes::from_utf8(content);
678-
let put: map = cap::artifact_put(token, payload, { purpose: "result" });
698+
let put: map = cap::artifact_put_result(token, payload, {});
679699
if map_bool(put, "ok", false) {
680700
let id: string = types::map_string(put, "id", "");
681701
let len: int = map_int(put, "len", content.length);
@@ -714,55 +734,42 @@ pub fn descriptor() -> map {
714734
pub fn validate(arguments: map) -> map {
715735
let mut result: map = { ok: true, code: "", message: "" };
716736
if arguments.has("pattern") == false {
717-
result = { ok: false, code: "invalid_arguments", message: "search_files requires a pattern" };
737+
result = { ok: false, code: "invalid_arguments", message: "search_files requires pattern" };
718738
} else {
719739
if type(arguments.pattern) != "string" {
720-
result = { ok: false, code: "invalid_arguments", message: "pattern must be a string" };
740+
result = { ok: false, code: "invalid_arguments", message: "search_files requires pattern" };
721741
} else {
722742
let pattern: string = arguments.pattern;
723743
if pattern.length == 0 {
724744
result = { ok: false, code: "invalid_arguments", message: "search_files requires a pattern" };
745+
}
746+
}
747+
}
748+
if map_bool(result, "ok", false) {
749+
if arguments.has("path") {
750+
if type(arguments.path) == "string" {
751+
result = validate_tool_path(arguments.path, true);
752+
}
753+
}
754+
}
755+
if map_bool(result, "ok", false) {
756+
if arguments.has("offset") {
757+
if type(arguments.offset) != "int" {
758+
result = { ok: false, code: "invalid_arguments", message: "numeric argument is invalid" };
725759
} else {
726-
if arguments.has("path") {
727-
if type(arguments.path) != "string" {
728-
result = { ok: false, code: "invalid_arguments", message: "path must be a string" };
729-
}
730-
}
731-
if map_bool(result, "ok", false) {
732-
if arguments.has("target") {
733-
if type(arguments.target) != "string" {
734-
result = { ok: false, code: "invalid_arguments", message: "target must be a string" };
735-
}
736-
}
760+
if arguments.offset < 0 {
761+
result = { ok: false, code: "invalid_arguments", message: "numeric argument is invalid" };
737762
}
738-
if map_bool(result, "ok", false) {
739-
if arguments.has("file_glob") {
740-
if type(arguments.file_glob) != "string" {
741-
result = { ok: false, code: "invalid_arguments", message: "file_glob must be a string" };
742-
}
743-
}
744-
}
745-
if map_bool(result, "ok", false) {
746-
if arguments.has("offset") {
747-
if type(arguments.offset) != "int" {
748-
result = { ok: false, code: "invalid_arguments", message: "offset must be an integer" };
749-
} else {
750-
if arguments.offset < 0 {
751-
result = { ok: false, code: "invalid_arguments", message: "offset must be an integer" };
752-
}
753-
}
754-
}
755-
}
756-
if map_bool(result, "ok", false) {
757-
if arguments.has("limit") {
758-
if type(arguments.limit) != "int" {
759-
result = { ok: false, code: "invalid_arguments", message: "limit must be an integer" };
760-
} else {
761-
if arguments.limit < 0 {
762-
result = { ok: false, code: "invalid_arguments", message: "limit must be an integer" };
763-
}
764-
}
765-
}
763+
}
764+
}
765+
}
766+
if map_bool(result, "ok", false) {
767+
if arguments.has("limit") {
768+
if type(arguments.limit) != "int" {
769+
result = { ok: false, code: "invalid_arguments", message: "numeric argument is invalid" };
770+
} else {
771+
if arguments.limit < 0 {
772+
result = { ok: false, code: "invalid_arguments", message: "numeric argument is invalid" };
766773
}
767774
}
768775
}
@@ -818,6 +825,7 @@ pub fn execute(context: map, arguments: map) -> map {
818825
fatal: false,
819826
fatal_code: "",
820827
fatal_message: "",
828+
start_ms: 0,
821829
max_search_files: map_int(config, "max_search_files", 10000),
822830
max_search_scanned_bytes: map_int(config, "max_search_scanned_bytes", 16777216),
823831
max_search_depth: map_int(config, "max_search_depth", 32),
@@ -826,27 +834,33 @@ pub fn execute(context: map, arguments: map) -> map {
826834
max_search_wall_time_ms: map_int(config, "max_search_wall_time_ms", 2000),
827835
max_read_bytes: map_int(config, "max_read_bytes", 1048576)
828836
};
829-
state = walk_search(state, path, 0);
830-
if map_bool(state, "fatal", false) {
831-
result = fail(types::map_string(state, "fatal_code", "internal_error"), types::map_string(state, "fatal_message", ""), {});
837+
let start_clock: map = cap::clock_monotonic_ms(token);
838+
if map_bool(start_clock, "ok", false) == false {
839+
result = fail_host(start_clock);
832840
} else {
833-
let collected: array = types::map_array(state, "lines");
834-
let sorted: array = sort_strings(collected);
835-
let selected: array = slice_lines(sorted, offset, limit);
836-
result = shrink_result(
837-
succeed(
838-
join_lines(selected),
839-
{
840-
match_count: selected.length,
841-
files_visited: map_int(state, "files_visited", 0),
842-
dirs_visited: map_int(state, "dirs_visited", 0)
843-
},
844-
map_bool(state, "truncated", false),
845-
[]
846-
),
847-
map_int(config, "max_tool_output_bytes", 65536),
848-
token
849-
);
841+
state.start_ms = map_int(start_clock, "ms", 0);
842+
state = walk_search(state, path, 0);
843+
if map_bool(state, "fatal", false) {
844+
result = fail(types::map_string(state, "fatal_code", "internal_error"), types::map_string(state, "fatal_message", ""), {});
845+
} else {
846+
let collected: array = types::map_array(state, "lines");
847+
let sorted: array = sort_strings(collected);
848+
let selected: array = slice_lines(sorted, offset, limit);
849+
result = shrink_result(
850+
succeed(
851+
join_lines(selected),
852+
{
853+
match_count: selected.length,
854+
files_visited: map_int(state, "files_visited", 0),
855+
dirs_visited: map_int(state, "dirs_visited", 0)
856+
},
857+
map_bool(state, "truncated", false),
858+
[]
859+
),
860+
map_int(config, "max_tool_output_bytes", 65536),
861+
token
862+
);
863+
}
850864
}
851865
}
852866
}

0 commit comments

Comments
 (0)