repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Mirrored from https://github.com/yingqi-z20/Agent-libOS
stars
latest
clone command
git clone gitlawb://did:key:z6MkqRzA...RfoM/yingqi-z20-Agen...git clone gitlawb://did:key:z6MkqRzA.../yingqi-z20-Agen...d98dd2c9IPC1d ago| #1 | from __future__ import annotations |
| #2 | |
| #3 | import asyncio |
| #4 | import inspect |
| #5 | from typing import Any, TYPE_CHECKING |
| #6 | |
| #7 | from agent_libos.config import DEFAULT_CONFIG, AgentLibOSConfig |
| #8 | from agent_libos.models import ( |
| #9 | CapabilityRight, |
| #10 | ForkMode, |
| #11 | HumanRequestStatus, |
| #12 | MemoryViewSpec, |
| #13 | MergePolicy, |
| #14 | ObjectHandle, |
| #15 | ObjectMetadata, |
| #16 | ObjectPatch, |
| #17 | ObjectRight, |
| #18 | ObjectType, |
| #19 | ProcessMessageKind, |
| #20 | ProcessSignal, |
| #21 | ProcessStatus, |
| #22 | ViewMode, |
| #23 | ) |
| #24 | from agent_libos.models.exceptions import ( |
| #25 | CapabilityDenied, |
| #26 | HumanApprovalRequired, |
| #27 | NotFound, |
| #28 | ProcessMessageWaitRequired, |
| #29 | ProcessWaitRequired, |
| #30 | ValidationError, |
| #31 | ) |
| #32 | from agent_libos.utils.serde import to_jsonable |
| #33 | |
| #34 | if TYPE_CHECKING: |
| #35 | from agent_libos.runtime.runtime import Runtime |
| #36 | |
| #37 | |
| #38 | class LibOSSyscallSession: |
| #39 | """Per JIT tool-call syscall session. |
| #40 | |
| #41 | Syscalls are libOS primitive calls made as the AgentProcess pid. They do not |
| #42 | consult the process tool table; the primitives remain responsible for |
| #43 | capability checks, human approval, audit, and events. |
| #44 | """ |
| #45 | |
| #46 | TERMINAL_STATUSES = {ProcessStatus.EXITED, ProcessStatus.FAILED, ProcessStatus.KILLED} |
| #47 | |
| #48 | def __init__(self, runtime: "Runtime", pid: str, config: AgentLibOSConfig | None = None) -> None: |
| #49 | self.runtime = runtime |
| #50 | self.pid = pid |
| #51 | self.config = config or DEFAULT_CONFIG |
| #52 | self._deferred_exit: dict[str, Any] | None = None |
| #53 | self._deferred_exec: dict[str, Any] | None = None |
| #54 | |
| #55 | async def handle(self, name: str, args: dict[str, Any]) -> Any: |
| #56 | normalized = name.strip() |
| #57 | if not normalized: |
| #58 | raise ValidationError("syscall name must be non-empty") |
| #59 | self.runtime.audit.record( |
| #60 | actor=self.pid, |
| #61 | action="syscall.request", |
| #62 | target=normalized, |
| #63 | decision={"args": to_jsonable(args)}, |
| #64 | ) |
| #65 | result = await self._with_blocking(lambda: self._dispatch(normalized, args)) |
| #66 | self.runtime.audit.record( |
| #67 | actor=self.pid, |
| #68 | action="syscall.result", |
| #69 | target=normalized, |
| #70 | decision={"ok": True}, |
| #71 | ) |
| #72 | return to_jsonable(result) |
| #73 | |
| #74 | async def apply_deferred_lifecycle(self, tool_result: ObjectHandle | None = None) -> None: |
| #75 | if self._deferred_exec is not None: |
| #76 | exec_args = self._deferred_exec |
| #77 | self.runtime.exec_process( |
| #78 | self.pid, |
| #79 | str(exec_args["image"]), |
| #80 | args=exec_args.get("args"), |
| #81 | goal=exec_args.get("goal"), |
| #82 | preserve_memory=bool(exec_args.get("preserve_memory", True)), |
| #83 | preserve_capabilities=bool(exec_args.get("preserve_capabilities", False)), |
| #84 | ) |
| #85 | if self._deferred_exit is not None: |
| #86 | exit_args = self._deferred_exit |
| #87 | result_handle = self._result_handle_for_exit(exit_args, tool_result) |
| #88 | self.runtime.process.exit( |
| #89 | self.pid, |
| #90 | result=result_handle, |
| #91 | failed=bool(exit_args.get("failed", False)), |
| #92 | message=exit_args.get("message"), |
| #93 | ) |
| #94 | |
| #95 | async def _with_blocking(self, operation: Any) -> Any: |
| #96 | while True: |
| #97 | try: |
| #98 | result = operation() |
| #99 | if inspect.isawaitable(result): |
| #100 | return await result |
| #101 | return result |
| #102 | except HumanApprovalRequired as exc: |
| #103 | await self._resolve_human_request(exc.request_id) |
| #104 | except ProcessWaitRequired as exc: |
| #105 | await self._wait_for_child_terminal(exc.child_pid) |
| #106 | except ProcessMessageWaitRequired as exc: |
| #107 | await self._wait_for_process_message(exc.recipient_pid, exc.filters) |
| #108 | |
| #109 | async def _resolve_human_request(self, request_id: str) -> None: |
| #110 | while True: |
| #111 | request = self.runtime.human.get(request_id) |
| #112 | if request.status == HumanRequestStatus.APPROVED: |
| #113 | return |
| #114 | if request.status != HumanRequestStatus.PENDING: |
| #115 | raise CapabilityDenied(f"human request was not approved: {request_id} status={request.status.value}") |
| #116 | processed = await self.runtime.human.aprocess_next_terminal( |
| #117 | human=request.human, |
| #118 | auto_approve=getattr(self.runtime, "_current_human_auto_approve", None), |
| #119 | auto_policy=getattr(self.runtime, "_current_human_auto_policy", None), |
| #120 | auto_answer=getattr(self.runtime, "_current_human_auto_answer", None), |
| #121 | ) |
| #122 | if processed is None: |
| #123 | await asyncio.sleep(self.runtime.scheduler.poll_interval_s) |
| #124 | |
| #125 | async def _wait_for_child_terminal(self, child_pid: str) -> None: |
| #126 | while True: |
| #127 | child = self.runtime.process.get(child_pid) |
| #128 | if child.status in self.TERMINAL_STATUSES: |
| #129 | return |
| #130 | await asyncio.sleep(self.runtime.scheduler.poll_interval_s) |
| #131 | |
| #132 | async def _wait_for_process_message(self, pid: str, filters: dict[str, Any]) -> None: |
| #133 | while True: |
| #134 | messages = self.runtime.messages.unread( |
| #135 | pid, |
| #136 | kind=filters.get("kind"), |
| #137 | sender=filters.get("sender"), |
| #138 | channel=filters.get("channel"), |
| #139 | correlation_id=filters.get("correlation_id"), |
| #140 | reply_to=filters.get("reply_to"), |
| #141 | message_ids=filters.get("message_ids"), |
| #142 | ) |
| #143 | if messages: |
| #144 | return |
| #145 | await asyncio.sleep(self.runtime.scheduler.poll_interval_s) |
| #146 | |
| #147 | def _dispatch(self, name: str, args: dict[str, Any]) -> Any: |
| #148 | if name in {"filesystem.read_text", "filesystem.read_text_file"}: |
| #149 | return self._filesystem_read_text(args) |
| #150 | if name in {"filesystem.write_text", "filesystem.write_text_file"}: |
| #151 | return self._filesystem_write_text(args) |
| #152 | if name in {"filesystem.read_directory", "filesystem.list_directory"}: |
| #153 | return self._filesystem_read_directory(args) |
| #154 | if name in {"filesystem.write_directory", "filesystem.make_directory"}: |
| #155 | return self._filesystem_write_directory(args) |
| #156 | if name == "filesystem.delete_file": |
| #157 | return self._filesystem_delete_file(args) |
| #158 | if name == "filesystem.delete_directory": |
| #159 | return self._filesystem_delete_directory(args) |
| #160 | if name == "memory.create_namespace": |
| #161 | ns = self.runtime.memory.create_namespace( |
| #162 | self.pid, |
| #163 | namespace=str(args["namespace"]), |
| #164 | parent_namespace=args.get("parent_namespace"), |
| #165 | metadata=dict(args.get("metadata") or {}), |
| #166 | ) |
| #167 | return {"namespace": ns.namespace, "parent_namespace": ns.parent_namespace, "created": True} |
| #168 | if name == "memory.list_namespace": |
| #169 | listing = self.runtime.memory.list_namespace(self.pid, args.get("namespace")) |
| #170 | return { |
| #171 | "namespace": listing["namespace"], |
| #172 | "objects": [ |
| #173 | { |
| #174 | "oid": obj.oid, |
| #175 | "namespace": obj.namespace, |
| #176 | "name": obj.name, |
| #177 | "type": obj.type.value, |
| #178 | "version": obj.version, |
| #179 | } |
| #180 | for obj in listing["objects"] |
| #181 | ], |
| #182 | "namespaces": [ |
| #183 | {"namespace": ns.namespace, "parent_namespace": ns.parent_namespace} |
| #184 | for ns in listing["namespaces"] |
| #185 | ], |
| #186 | } |
| #187 | if name == "memory.create_object": |
| #188 | return self._memory_create_object(args) |
| #189 | if name in {"memory.read_object", "memory.get_object"}: |
| #190 | obj = self.runtime.memory.get_object_by_name(self.pid, str(args["name"]), namespace=args.get("namespace")) |
| #191 | return { |
| #192 | "oid": obj.oid, |
| #193 | "namespace": obj.namespace, |
| #194 | "name": obj.name, |
| #195 | "type": obj.type.value, |
| #196 | "version": obj.version, |
| #197 | "payload": obj.payload, |
| #198 | } |
| #199 | if name in {"memory.append_object", "memory.append_memory_object"}: |
| #200 | return self._memory_append_object(args) |
| #201 | if name in {"human.output", "human_output"}: |
| #202 | return self.runtime.human.output( |
| #203 | pid=self.pid, |
| #204 | message=str(args.get("message", "")), |
| #205 | human=str(args.get("human") or self.config.runtime.default_human), |
| #206 | channel=str(args.get("channel") or self.config.runtime.terminal_channel), |
| #207 | ) |
| #208 | if name in {"human.ask", "ask_human"}: |
| #209 | return self._human_ask(args) |
| #210 | if name in {"human.request_permission", "permission.request", "request_permission"}: |
| #211 | return self._request_permission(args) |
| #212 | if name in {"clock.now", "time.now"}: |
| #213 | return self.runtime.clock.now(self.pid, tz=str(args.get("timezone") or self.config.tools.clock_timezone)) |
| #214 | if name in {"clock.sleep", "sleep"}: |
| #215 | return self.runtime.clock.asleep(self.pid, float(args.get("seconds", 0))) |
| #216 | if name in {"process.get_working_directory", "process.cwd"}: |
| #217 | return {"working_directory": self.runtime.process.working_directory(self.pid)} |
| #218 | if name in {"process.set_working_directory", "process.chdir"}: |
| #219 | process = self.runtime.set_process_working_directory(self.pid, str(args["path"])) |
| #220 | return {"working_directory": process.working_directory} |
| #221 | if name == "process.fork": |
| #222 | return self._process_fork(args) |
| #223 | if name == "process.spawn_child": |
| #224 | return self._process_spawn_child(args) |
| #225 | if name == "process.wait": |
| #226 | return self._process_wait(args) |
| #227 | if name == "process.list_children": |
| #228 | return { |
| #229 | "children": [ |
| #230 | { |
| #231 | "pid": child.pid, |
| #232 | "image": child.image_id, |
| #233 | "status": child.status.value, |
| #234 | "working_directory": child.working_directory, |
| #235 | "goal_oid": child.goal_oid, |
| #236 | "status_message": child.status_message, |
| #237 | } |
| #238 | for child in self.runtime.process.list_children( |
| #239 | self.pid, |
| #240 | include_terminal=bool(args.get("include_terminal", True)), |
| #241 | ) |
| #242 | ] |
| #243 | } |
| #244 | if name == "process.signal": |
| #245 | child = self.runtime.process.signal_child( |
| #246 | self.pid, |
| #247 | str(args["child_pid"]), |
| #248 | ProcessSignal(str(args["signal"])), |
| #249 | reason=args.get("reason"), |
| #250 | ) |
| #251 | return {"child_pid": child.pid, "status": child.status.value, "signal": str(args["signal"])} |
| #252 | if name == "process.merge_child_memory": |
| #253 | result = self.runtime.process.merge_child_memory( |
| #254 | self.pid, |
| #255 | str(args["child_pid"]), |
| #256 | policy=MergePolicy(include_child_created=bool(args.get("include_child_created", True))), |
| #257 | ) |
| #258 | return result |
| #259 | if name == "process.send_message": |
| #260 | message = self.runtime.messages.send_from_process( |
| #261 | self.pid, |
| #262 | str(args["recipient_pid"]), |
| #263 | kind=ProcessMessageKind(str(args.get("kind", ProcessMessageKind.NORMAL.value))), |
| #264 | channel=str(args.get("channel", "default")), |
| #265 | correlation_id=str(args["correlation_id"]) if args.get("correlation_id") is not None else None, |
| #266 | reply_to=str(args["reply_to"]) if args.get("reply_to") is not None else None, |
| #267 | subject=str(args.get("subject", "")), |
| #268 | body=str(args.get("body", "")), |
| #269 | payload=dict(args.get("payload") or {}), |
| #270 | ) |
| #271 | return self._process_message_result(message) |
| #272 | if name == "process.read_messages": |
| #273 | return self._process_read_messages(args, default_block=False) |
| #274 | if name == "process.receive_messages": |
| #275 | return self._process_read_messages(args, default_block=True) |
| #276 | if name == "process.exec": |
| #277 | self._deferred_exec = dict(args) |
| #278 | return {"deferred": True, "operation": "process.exec", "image": args.get("image")} |
| #279 | if name == "process.exit": |
| #280 | self._deferred_exit = dict(args) |
| #281 | return {"deferred": True, "operation": "process.exit"} |
| #282 | if name in {"shell.run", "shell.run_command"}: |
| #283 | cwd = self.runtime.process.working_directory(self.pid) |
| #284 | return self.runtime.shell.arun( |
| #285 | self.pid, |
| #286 | self._string_list_arg(args, "argv"), |
| #287 | timeout=float(args.get("timeout_s", self.config.tools.shell_timeout_s)), |
| #288 | cwd=cwd, |
| #289 | ) |
| #290 | if name == "image.register": |
| #291 | result = self.runtime.image_registry.register( |
| #292 | dict(args["image"]), |
| #293 | actor=self.pid, |
| #294 | replace=bool(args.get("replace", False)), |
| #295 | require_capability=True, |
| #296 | source=args.get("source"), |
| #297 | ) |
| #298 | return self._image_result(result) |
| #299 | if name == "image.load_yaml": |
| #300 | return self._image_load_yaml(args) |
| #301 | raise NotFound(f"unknown libOS syscall: {name}") |
| #302 | |
| #303 | def _filesystem_read_text(self, args: dict[str, Any]) -> Any: |
| #304 | cwd = self.runtime.process.working_directory(self.pid) |
| #305 | return self.runtime.filesystem.read_text( |
| #306 | pid=self.pid, |
| #307 | path=str(args["path"]), |
| #308 | encoding=str(args.get("encoding") or self.config.tools.default_text_encoding), |
| #309 | max_bytes=int(args.get("max_bytes", self.config.tools.filesystem_read_max_bytes)), |
| #310 | cwd=cwd, |
| #311 | ) |
| #312 | |
| #313 | def _filesystem_write_text(self, args: dict[str, Any]) -> Any: |
| #314 | cwd = self.runtime.process.working_directory(self.pid) |
| #315 | return self.runtime.filesystem.write_text( |
| #316 | pid=self.pid, |
| #317 | path=str(args["path"]), |
| #318 | text=str(args.get("content", args.get("text", ""))), |
| #319 | encoding=str(args.get("encoding") or self.config.tools.default_text_encoding), |
| #320 | overwrite=bool(args.get("overwrite", True)), |
| #321 | cwd=cwd, |
| #322 | ) |
| #323 | |
| #324 | def _filesystem_read_directory(self, args: dict[str, Any]) -> Any: |
| #325 | cwd = self.runtime.process.working_directory(self.pid) |
| #326 | return self.runtime.filesystem.read_directory( |
| #327 | pid=self.pid, |
| #328 | path=str(args["path"]), |
| #329 | limit=int(args.get("limit", self.config.tools.directory_entry_limit)), |
| #330 | cwd=cwd, |
| #331 | ) |
| #332 | |
| #333 | def _filesystem_write_directory(self, args: dict[str, Any]) -> Any: |
| #334 | cwd = self.runtime.process.working_directory(self.pid) |
| #335 | return self.runtime.filesystem.write_directory( |
| #336 | pid=self.pid, |
| #337 | path=str(args["path"]), |
| #338 | parents=bool(args.get("parents", True)), |
| #339 | exist_ok=bool(args.get("exist_ok", True)), |
| #340 | cwd=cwd, |
| #341 | ) |
| #342 | |
| #343 | def _filesystem_delete_file(self, args: dict[str, Any]) -> Any: |
| #344 | cwd = self.runtime.process.working_directory(self.pid) |
| #345 | return self.runtime.filesystem.delete_file( |
| #346 | pid=self.pid, |
| #347 | path=str(args["path"]), |
| #348 | missing_ok=bool(args.get("missing_ok", False)), |
| #349 | cwd=cwd, |
| #350 | ) |
| #351 | |
| #352 | def _filesystem_delete_directory(self, args: dict[str, Any]) -> Any: |
| #353 | cwd = self.runtime.process.working_directory(self.pid) |
| #354 | return self.runtime.filesystem.delete_directory( |
| #355 | pid=self.pid, |
| #356 | path=str(args["path"]), |
| #357 | recursive=bool(args.get("recursive", False)), |
| #358 | missing_ok=bool(args.get("missing_ok", False)), |
| #359 | cwd=cwd, |
| #360 | ) |
| #361 | |
| #362 | def _memory_create_object(self, args: dict[str, Any]) -> Any: |
| #363 | metadata_arg = dict(args.get("metadata") or {}) |
| #364 | metadata = ObjectMetadata( |
| #365 | title=metadata_arg.get("title"), |
| #366 | summary=metadata_arg.get("summary"), |
| #367 | tags=list(metadata_arg.get("tags", [])), |
| #368 | mime_type=metadata_arg.get("mime_type"), |
| #369 | ) |
| #370 | handle = self.runtime.memory.create_object( |
| #371 | pid=self.pid, |
| #372 | object_type=ObjectType(str(args.get("type", ObjectType.OBSERVATION.value))), |
| #373 | payload=args.get("payload"), |
| #374 | metadata=metadata, |
| #375 | immutable=bool(args.get("immutable", True)), |
| #376 | name=args.get("name"), |
| #377 | namespace=args.get("namespace"), |
| #378 | ) |
| #379 | self._add_handle_to_view(handle) |
| #380 | obj = self.runtime.memory.get_object(self.pid, handle) |
| #381 | return {"oid": handle.oid, "namespace": obj.namespace, "name": obj.name, "type": obj.type.value} |
| #382 | |
| #383 | def _memory_append_object(self, args: dict[str, Any]) -> Any: |
| #384 | handle = self.runtime.memory.handle_for_name( |
| #385 | self.pid, |
| #386 | str(args["name"]), |
| #387 | rights=["read", "write"], |
| #388 | issued_by="jit.syscall", |
| #389 | namespace=args.get("namespace"), |
| #390 | ) |
| #391 | obj = self.runtime.memory.get_object(self.pid, handle) |
| #392 | payload = obj.payload |
| #393 | list_field = str(args.get("list_field", "entries")) |
| #394 | if isinstance(payload, dict): |
| #395 | values = payload.setdefault(list_field, []) |
| #396 | if not isinstance(values, list): |
| #397 | raise ValidationError("target object list_field is not a list") |
| #398 | values.append(args.get("entry")) |
| #399 | length = len(values) |
| #400 | elif isinstance(payload, list): |
| #401 | payload.append(args.get("entry")) |
| #402 | list_field = "" |
| #403 | length = len(payload) |
| #404 | else: |
| #405 | raise ValidationError("target object payload is not appendable") |
| #406 | self.runtime.memory.update_object(self.pid, handle, ObjectPatch(payload=payload)) |
| #407 | updated = self.runtime.memory.get_object(self.pid, handle) |
| #408 | return { |
| #409 | "oid": updated.oid, |
| #410 | "namespace": updated.namespace, |
| #411 | "name": updated.name, |
| #412 | "version": updated.version, |
| #413 | "list_field": list_field or None, |
| #414 | "length": length, |
| #415 | } |
| #416 | |
| #417 | def _human_ask(self, args: dict[str, Any]) -> Any: |
| #418 | request_id = self.runtime.human.ask( |
| #419 | pid=self.pid, |
| #420 | human=str(args.get("human") or self.config.runtime.default_human), |
| #421 | question=str(args["question"]), |
| #422 | context=dict(args.get("context") or {}), |
| #423 | blocking=True, |
| #424 | ) |
| #425 | return self._answer_human_question(request_id) |
| #426 | |
| #427 | def _request_permission(self, args: dict[str, Any]) -> Any: |
| #428 | request_id = self.runtime.human.request_permission( |
| #429 | pid=self.pid, |
| #430 | human=str(args.get("human") or self.config.runtime.default_human), |
| #431 | resource=str(args["resource"]), |
| #432 | rights=[str(right) for right in args.get("rights", [])], |
| #433 | reason=str(args.get("reason", "")), |
| #434 | blocking=True, |
| #435 | ) |
| #436 | return self._permission_request_result(request_id, args) |
| #437 | |
| #438 | async def _answer_human_question(self, request_id: str) -> dict[str, Any]: |
| #439 | await self._resolve_human_request(request_id) |
| #440 | answer = self.runtime.human.answer_for_request(request_id) |
| #441 | return {"request_id": request_id, "answer": answer, "status": "answered"} |
| #442 | |
| #443 | async def _permission_request_result(self, request_id: str, args: dict[str, Any]) -> dict[str, Any]: |
| #444 | await self._resolve_human_request(request_id) |
| #445 | request = self.runtime.human.get(request_id) |
| #446 | return { |
| #447 | "request_id": request_id, |
| #448 | "resource": str(args["resource"]), |
| #449 | "rights": [str(right) for right in args.get("rights", [])], |
| #450 | "status": request.status.value, |
| #451 | "decision": request.decision, |
| #452 | } |
| #453 | |
| #454 | def _process_fork(self, args: dict[str, Any]) -> Any: |
| #455 | mode = ForkMode(str(args.get("mode", ForkMode.WORKER.value))) |
| #456 | child_pid = self.runtime.process.fork( |
| #457 | parent=self.pid, |
| #458 | goal=args.get("goal", ""), |
| #459 | memory_view=MemoryViewSpec( |
| #460 | roots=self._selected_roots(args.get("root_oids")), |
| #461 | mode=self._view_mode_for_fork(mode), |
| #462 | include_parent_roots=bool(args.get("include_parent_roots", True)), |
| #463 | ), |
| #464 | inherit_capabilities=list(args.get("inherit_capabilities") or []), |
| #465 | image=args.get("image"), |
| #466 | mode=mode, |
| #467 | working_directory=args.get("working_directory"), |
| #468 | ) |
| #469 | child = self.runtime.process.get(child_pid) |
| #470 | return {"child_pid": child.pid, "status": child.status.value, "image": child.image_id, "goal_oid": child.goal_oid} |
| #471 | |
| #472 | def _process_spawn_child(self, args: dict[str, Any]) -> Any: |
| #473 | child_pid = self.runtime.spawn_child_process( |
| #474 | parent=self.pid, |
| #475 | goal=args.get("goal", ""), |
| #476 | image=args.get("image"), |
| #477 | inherit_capabilities=list(args.get("inherit_capabilities") or []), |
| #478 | working_directory=args.get("working_directory"), |
| #479 | ) |
| #480 | child = self.runtime.process.get(child_pid) |
| #481 | return {"child_pid": child.pid, "status": child.status.value, "image": child.image_id, "goal_oid": child.goal_oid} |
| #482 | |
| #483 | def _process_wait(self, args: dict[str, Any]) -> Any: |
| #484 | try: |
| #485 | result = self.runtime.process.wait( |
| #486 | self.pid, |
| #487 | str(args["child_pid"]), |
| #488 | timeout=None if bool(args.get("block", True)) else 0, |
| #489 | ) |
| #490 | except TimeoutError: |
| #491 | child = self.runtime.process.get(str(args["child_pid"])) |
| #492 | return {"child_pid": child.pid, "status": child.status.value, "ready": False, "message": child.status_message} |
| #493 | return { |
| #494 | "child_pid": result.pid, |
| #495 | "status": result.status.value, |
| #496 | "ready": True, |
| #497 | "result_oid": result.result.oid if result.result is not None else None, |
| #498 | "message": result.message, |
| #499 | } |
| #500 | |
| #501 | def _process_read_messages(self, args: dict[str, Any], *, default_block: bool) -> dict[str, Any]: |
| #502 | kind = ProcessMessageKind(str(args["kind"])) if args.get("kind") is not None else None |
| #503 | messages = self.runtime.messages.receive( |
| #504 | self.pid, |
| #505 | block=bool(args.get("block", default_block)), |
| #506 | include_acked=bool(args.get("include_acked", False)), |
| #507 | kind=kind, |
| #508 | sender=str(args["sender"]) if args.get("sender") is not None else None, |
| #509 | channel=str(args["channel"]) if args.get("channel") is not None else None, |
| #510 | correlation_id=str(args["correlation_id"]) if args.get("correlation_id") is not None else None, |
| #511 | reply_to=str(args["reply_to"]) if args.get("reply_to") is not None else None, |
| #512 | message_ids=[str(item) for item in args["message_ids"]] if args.get("message_ids") is not None else None, |
| #513 | limit=int(args["limit"]) if args.get("limit") is not None else None, |
| #514 | ) |
| #515 | acked = [] |
| #516 | if bool(args.get("ack", True)): |
| #517 | unread_ids = [message.message_id for message in messages if message.status.value == "unread"] |
| #518 | if unread_ids: |
| #519 | acked = self.runtime.messages.ack(self.pid, unread_ids) |
| #520 | acked_by_id = {message.message_id: message for message in acked} |
| #521 | messages = [acked_by_id.get(message.message_id, message) for message in messages] |
| #522 | return { |
| #523 | "ready": bool(messages), |
| #524 | "messages": [self._process_message_result(message) for message in messages], |
| #525 | "acked_message_ids": [message.message_id for message in acked], |
| #526 | } |
| #527 | |
| #528 | def _process_message_result(self, message: Any) -> dict[str, Any]: |
| #529 | return { |
| #530 | "message_id": message.message_id, |
| #531 | "sender": message.sender, |
| #532 | "recipient_pid": message.recipient_pid, |
| #533 | "kind": message.kind.value, |
| #534 | "channel": message.channel, |
| #535 | "correlation_id": message.correlation_id, |
| #536 | "reply_to": message.reply_to, |
| #537 | "subject": message.subject, |
| #538 | "body": message.body, |
| #539 | "payload": message.payload, |
| #540 | "status": message.status.value, |
| #541 | "created_at": message.created_at, |
| #542 | "acked_at": message.acked_at, |
| #543 | } |
| #544 | |
| #545 | def _image_load_yaml(self, args: dict[str, Any]) -> Any: |
| #546 | file_result = self._filesystem_read_text( |
| #547 | { |
| #548 | "path": args["path"], |
| #549 | "encoding": args.get("encoding", self.config.tools.default_text_encoding), |
| #550 | "max_bytes": args.get("max_bytes", self.config.image.yaml_max_bytes), |
| #551 | } |
| #552 | ) |
| #553 | if file_result.truncated: |
| #554 | raise ValidationError("image YAML exceeded max_bytes") |
| #555 | result = self.runtime.image_registry.register_from_yaml_text( |
| #556 | file_result.content, |
| #557 | actor=self.pid, |
| #558 | replace=bool(args.get("replace", False)), |
| #559 | require_capability=True, |
| #560 | source=file_result.path, |
| #561 | ) |
| #562 | return self._image_result(result) |
| #563 | |
| #564 | def _image_result(self, result: Any) -> dict[str, Any]: |
| #565 | image = result.image |
| #566 | return { |
| #567 | "image_id": image.image_id, |
| #568 | "name": image.name, |
| #569 | "version": image.version, |
| #570 | "replaced": result.replaced, |
| #571 | "source": result.source, |
| #572 | "default_tools": list(image.default_tools), |
| #573 | "required_capabilities_count": len(image.required_capabilities), |
| #574 | } |
| #575 | |
| #576 | def _string_list_arg(self, args: dict[str, Any], key: str) -> list[str]: |
| #577 | value = args.get(key, []) |
| #578 | if not isinstance(value, list): |
| #579 | raise ValidationError(f"{key} must be a list of strings") |
| #580 | if not all(isinstance(item, str) for item in value): |
| #581 | raise ValidationError(f"{key} must contain only strings") |
| #582 | return list(value) |
| #583 | |
| #584 | def _result_handle_for_exit(self, args: dict[str, Any], tool_result: ObjectHandle | None) -> ObjectHandle | None: |
| #585 | if args.get("result_oid"): |
| #586 | return self.runtime.capability.handle_for_object( |
| #587 | self.pid, |
| #588 | str(args["result_oid"]), |
| #589 | {"read", "materialize", "link", "diff"}, |
| #590 | issued_by="jit.process_exit", |
| #591 | ) |
| #592 | if "payload" in args: |
| #593 | return self.runtime.memory.create_object( |
| #594 | pid=self.pid, |
| #595 | object_type=ObjectType.SUMMARY, |
| #596 | payload=args.get("payload"), |
| #597 | metadata=ObjectMetadata(title="Process final result", tags=["final", "jit"]), |
| #598 | ) |
| #599 | if bool(args.get("use_tool_result", False)): |
| #600 | return tool_result |
| #601 | return None |
| #602 | |
| #603 | def _selected_roots(self, root_oids: Any) -> list[ObjectHandle] | None: |
| #604 | if root_oids is None: |
| #605 | return None |
| #606 | process = self.runtime.process.get(self.pid) |
| #607 | visible = {handle.oid: handle for handle in (process.memory_view.roots if process.memory_view else [])} |
| #608 | roots: list[ObjectHandle] = [] |
| #609 | for oid in root_oids: |
| #610 | oid_text = str(oid) |
| #611 | if oid_text in visible: |
| #612 | roots.append(visible[oid_text]) |
| #613 | continue |
| #614 | self.runtime.capability.require(self.pid, f"object:{oid_text}", ObjectRight.READ) |
| #615 | roots.append( |
| #616 | self.runtime.capability.handle_for_object( |
| #617 | self.pid, |
| #618 | oid_text, |
| #619 | {"read", "materialize", "diff"}, |
| #620 | issued_by="jit.syscall.fork", |
| #621 | ) |
| #622 | ) |
| #623 | return roots |
| #624 | |
| #625 | def _add_handle_to_view(self, handle: ObjectHandle) -> None: |
| #626 | process = self.runtime.process.get(self.pid) |
| #627 | if process.memory_view is None: |
| #628 | process.memory_view = self.runtime.memory.create_view(self.pid, [handle], mode=ViewMode.READ_ONLY) |
| #629 | elif all(existing.oid != handle.oid for existing in process.memory_view.roots): |
| #630 | process.memory_view.roots.append(handle) |
| #631 | self.runtime.store.update_process(process) |
| #632 | |
| #633 | def _view_mode_for_fork(self, mode: ForkMode) -> ViewMode: |
| #634 | if mode == ForkMode.COPY: |
| #635 | return ViewMode.COPY_ON_WRITE |
| #636 | if mode == ForkMode.SPECULATIVE: |
| #637 | return ViewMode.EPHEMERAL |
| #638 | return ViewMode.READ_ONLY |
| #639 |