agentfs-sdk 0.4.0__py3-none-any.whl → 0.4.0rc2__py3-none-any.whl
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.
- agentfs_sdk/__init__.py +2 -10
- agentfs_sdk/filesystem.py +51 -643
- {agentfs_sdk-0.4.0.dist-info → agentfs_sdk-0.4.0rc2.dist-info}/METADATA +2 -4
- agentfs_sdk-0.4.0rc2.dist-info/RECORD +9 -0
- agentfs_sdk/constants.py +0 -13
- agentfs_sdk/errors.py +0 -60
- agentfs_sdk/guards.py +0 -199
- agentfs_sdk-0.4.0.dist-info/RECORD +0 -12
- {agentfs_sdk-0.4.0.dist-info → agentfs_sdk-0.4.0rc2.dist-info}/WHEEL +0 -0
- {agentfs_sdk-0.4.0.dist-info → agentfs_sdk-0.4.0rc2.dist-info}/top_level.txt +0 -0
agentfs_sdk/__init__.py
CHANGED
|
@@ -4,12 +4,11 @@ A filesystem and key-value store for AI agents, powered by SQLite.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from .agentfs import AgentFS, AgentFSOptions
|
|
7
|
-
from .
|
|
8
|
-
from .filesystem import S_IFDIR, S_IFLNK, S_IFMT, S_IFREG, Filesystem, Stats
|
|
7
|
+
from .filesystem import Filesystem, Stats
|
|
9
8
|
from .kvstore import KvStore
|
|
10
9
|
from .toolcalls import ToolCall, ToolCalls, ToolCallStats
|
|
11
10
|
|
|
12
|
-
__version__ = "0.4.0"
|
|
11
|
+
__version__ = "0.4.0-pre.2"
|
|
13
12
|
|
|
14
13
|
__all__ = [
|
|
15
14
|
"AgentFS",
|
|
@@ -17,14 +16,7 @@ __all__ = [
|
|
|
17
16
|
"KvStore",
|
|
18
17
|
"Filesystem",
|
|
19
18
|
"Stats",
|
|
20
|
-
"S_IFDIR",
|
|
21
|
-
"S_IFLNK",
|
|
22
|
-
"S_IFMT",
|
|
23
|
-
"S_IFREG",
|
|
24
19
|
"ToolCalls",
|
|
25
20
|
"ToolCall",
|
|
26
21
|
"ToolCallStats",
|
|
27
|
-
"ErrnoException",
|
|
28
|
-
"FsErrorCode",
|
|
29
|
-
"FsSyscall",
|
|
30
22
|
]
|
agentfs_sdk/filesystem.py
CHANGED
|
@@ -6,31 +6,17 @@ from typing import List, Optional, Union
|
|
|
6
6
|
|
|
7
7
|
from turso.aio import Connection
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
assert_inode_is_directory,
|
|
21
|
-
assert_not_root,
|
|
22
|
-
assert_not_symlink_mode,
|
|
23
|
-
assert_readable_existing_inode,
|
|
24
|
-
assert_readdir_target_inode,
|
|
25
|
-
assert_unlink_target_inode,
|
|
26
|
-
assert_writable_existing_inode,
|
|
27
|
-
get_inode_mode_or_throw,
|
|
28
|
-
normalize_rm_options,
|
|
29
|
-
throw_enoent_unless_force,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
# Re-export constants for backwards compatibility
|
|
33
|
-
__all__ = ["Filesystem", "Stats", "S_IFMT", "S_IFREG", "S_IFDIR", "S_IFLNK"]
|
|
9
|
+
# File types for mode field
|
|
10
|
+
S_IFMT = 0o170000 # File type mask
|
|
11
|
+
S_IFREG = 0o100000 # Regular file
|
|
12
|
+
S_IFDIR = 0o040000 # Directory
|
|
13
|
+
S_IFLNK = 0o120000 # Symbolic link
|
|
14
|
+
|
|
15
|
+
# Default permissions
|
|
16
|
+
DEFAULT_FILE_MODE = S_IFREG | 0o644 # Regular file, rw-r--r--
|
|
17
|
+
DEFAULT_DIR_MODE = S_IFDIR | 0o755 # Directory, rwxr-xr-x
|
|
18
|
+
|
|
19
|
+
DEFAULT_CHUNK_SIZE = 4096
|
|
34
20
|
|
|
35
21
|
|
|
36
22
|
@dataclass
|
|
@@ -115,7 +101,6 @@ class Filesystem:
|
|
|
115
101
|
CREATE TABLE IF NOT EXISTS fs_inode (
|
|
116
102
|
ino INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
117
103
|
mode INTEGER NOT NULL,
|
|
118
|
-
nlink INTEGER NOT NULL DEFAULT 0,
|
|
119
104
|
uid INTEGER NOT NULL DEFAULT 0,
|
|
120
105
|
gid INTEGER NOT NULL DEFAULT 0,
|
|
121
106
|
size INTEGER NOT NULL DEFAULT 0,
|
|
@@ -176,8 +161,8 @@ class Filesystem:
|
|
|
176
161
|
now = int(time.time())
|
|
177
162
|
await self._db.execute(
|
|
178
163
|
"""
|
|
179
|
-
INSERT INTO fs_inode (ino, mode,
|
|
180
|
-
VALUES (?, ?,
|
|
164
|
+
INSERT INTO fs_inode (ino, mode, uid, gid, size, atime, mtime, ctime)
|
|
165
|
+
VALUES (?, ?, 0, 0, 0, ?, ?, ?)
|
|
181
166
|
""",
|
|
182
167
|
(self._root_ino, DEFAULT_DIR_MODE, now, now, now),
|
|
183
168
|
)
|
|
@@ -281,11 +266,6 @@ class Filesystem:
|
|
|
281
266
|
""",
|
|
282
267
|
(name, parent_ino, ino),
|
|
283
268
|
)
|
|
284
|
-
# Increment link count
|
|
285
|
-
await self._db.execute(
|
|
286
|
-
"UPDATE fs_inode SET nlink = nlink + 1 WHERE ino = ?",
|
|
287
|
-
(ino,),
|
|
288
|
-
)
|
|
289
269
|
await self._db.commit()
|
|
290
270
|
|
|
291
271
|
async def _ensure_parent_dirs(self, path: str) -> None:
|
|
@@ -321,41 +301,16 @@ class Filesystem:
|
|
|
321
301
|
|
|
322
302
|
async def _get_link_count(self, ino: int) -> int:
|
|
323
303
|
"""Get link count for an inode"""
|
|
324
|
-
cursor = await self._db.execute("SELECT
|
|
304
|
+
cursor = await self._db.execute("SELECT COUNT(*) FROM fs_dentry WHERE ino = ?", (ino,))
|
|
325
305
|
result = await cursor.fetchone()
|
|
326
306
|
return result[0] if result else 0
|
|
327
307
|
|
|
328
|
-
async def
|
|
329
|
-
"""Get mode for an inode"""
|
|
330
|
-
cursor = await self._db.execute("SELECT mode FROM fs_inode WHERE ino = ?", (ino,))
|
|
331
|
-
row = await cursor.fetchone()
|
|
332
|
-
return row[0] if row else None
|
|
333
|
-
|
|
334
|
-
async def _resolve_path_or_throw(self, path: str, syscall: FsSyscall) -> tuple[str, int]:
|
|
335
|
-
"""Resolve path to inode or throw ENOENT"""
|
|
336
|
-
normalized_path = self._normalize_path(path)
|
|
337
|
-
ino = await self._resolve_path(normalized_path)
|
|
338
|
-
if ino is None:
|
|
339
|
-
raise ErrnoException(
|
|
340
|
-
code="ENOENT",
|
|
341
|
-
syscall=syscall,
|
|
342
|
-
path=normalized_path,
|
|
343
|
-
message="no such file or directory",
|
|
344
|
-
)
|
|
345
|
-
return (normalized_path, ino)
|
|
346
|
-
|
|
347
|
-
async def write_file(
|
|
348
|
-
self,
|
|
349
|
-
path: str,
|
|
350
|
-
content: Union[str, bytes],
|
|
351
|
-
encoding: str = "utf-8",
|
|
352
|
-
) -> None:
|
|
308
|
+
async def write_file(self, path: str, content: Union[str, bytes]) -> None:
|
|
353
309
|
"""Write content to a file
|
|
354
310
|
|
|
355
311
|
Args:
|
|
356
312
|
path: Path to the file
|
|
357
313
|
content: Content to write (string or bytes)
|
|
358
|
-
encoding: Text encoding (default: 'utf-8')
|
|
359
314
|
|
|
360
315
|
Example:
|
|
361
316
|
>>> await fs.write_file('/data/config.json', '{"key": "value"}')
|
|
@@ -363,31 +318,20 @@ class Filesystem:
|
|
|
363
318
|
# Ensure parent directories exist
|
|
364
319
|
await self._ensure_parent_dirs(path)
|
|
365
320
|
|
|
366
|
-
normalized_path = self._normalize_path(path)
|
|
367
321
|
# Check if file already exists
|
|
368
|
-
ino = await self._resolve_path(
|
|
322
|
+
ino = await self._resolve_path(path)
|
|
369
323
|
|
|
370
324
|
if ino is not None:
|
|
371
|
-
# Validate existing inode
|
|
372
|
-
await assert_writable_existing_inode(self._db, ino, "open", normalized_path)
|
|
373
325
|
# Update existing file
|
|
374
|
-
await self._update_file_content(ino, content
|
|
326
|
+
await self._update_file_content(ino, content)
|
|
375
327
|
else:
|
|
376
328
|
# Create new file
|
|
377
|
-
parent = await self._resolve_parent(
|
|
329
|
+
parent = await self._resolve_parent(path)
|
|
378
330
|
if not parent:
|
|
379
|
-
raise
|
|
380
|
-
code="ENOENT",
|
|
381
|
-
syscall="open",
|
|
382
|
-
path=normalized_path,
|
|
383
|
-
message="no such file or directory",
|
|
384
|
-
)
|
|
331
|
+
raise FileNotFoundError(f"ENOENT: parent directory does not exist: {path}")
|
|
385
332
|
|
|
386
333
|
parent_ino, name = parent
|
|
387
334
|
|
|
388
|
-
# Ensure parent is a directory
|
|
389
|
-
await assert_inode_is_directory(self._db, parent_ino, "open", normalized_path)
|
|
390
|
-
|
|
391
335
|
# Create inode
|
|
392
336
|
file_ino = await self._create_inode(DEFAULT_FILE_MODE)
|
|
393
337
|
|
|
@@ -395,13 +339,11 @@ class Filesystem:
|
|
|
395
339
|
await self._create_dentry(parent_ino, name, file_ino)
|
|
396
340
|
|
|
397
341
|
# Write content
|
|
398
|
-
await self._update_file_content(file_ino, content
|
|
342
|
+
await self._update_file_content(file_ino, content)
|
|
399
343
|
|
|
400
|
-
async def _update_file_content(
|
|
401
|
-
self, ino: int, content: Union[str, bytes], encoding: str = "utf-8"
|
|
402
|
-
) -> None:
|
|
344
|
+
async def _update_file_content(self, ino: int, content: Union[str, bytes]) -> None:
|
|
403
345
|
"""Update file content"""
|
|
404
|
-
buffer = content.encode(
|
|
346
|
+
buffer = content.encode("utf-8") if isinstance(content, str) else content
|
|
405
347
|
now = int(time.time())
|
|
406
348
|
|
|
407
349
|
# Delete existing data chunks
|
|
@@ -446,9 +388,9 @@ class Filesystem:
|
|
|
446
388
|
>>> content = await fs.read_file('/data/config.json')
|
|
447
389
|
>>> data = await fs.read_file('/data/image.png', encoding=None)
|
|
448
390
|
"""
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
391
|
+
ino = await self._resolve_path(path)
|
|
392
|
+
if ino is None:
|
|
393
|
+
raise FileNotFoundError(f"ENOENT: no such file or directory, open '{path}'")
|
|
452
394
|
|
|
453
395
|
# Get all data chunks
|
|
454
396
|
cursor = await self._db.execute(
|
|
@@ -490,9 +432,9 @@ class Filesystem:
|
|
|
490
432
|
>>> for entry in entries:
|
|
491
433
|
>>> print(entry)
|
|
492
434
|
"""
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
435
|
+
ino = await self._resolve_path(path)
|
|
436
|
+
if ino is None:
|
|
437
|
+
raise FileNotFoundError(f"ENOENT: no such file or directory, scandir '{path}'")
|
|
496
438
|
|
|
497
439
|
# Get all directory entries
|
|
498
440
|
cursor = await self._db.execute(
|
|
@@ -507,24 +449,23 @@ class Filesystem:
|
|
|
507
449
|
|
|
508
450
|
return [row[0] for row in rows]
|
|
509
451
|
|
|
510
|
-
async def
|
|
511
|
-
"""Delete a file
|
|
452
|
+
async def delete_file(self, path: str) -> None:
|
|
453
|
+
"""Delete a file
|
|
512
454
|
|
|
513
455
|
Args:
|
|
514
456
|
path: Path to the file
|
|
515
457
|
|
|
516
458
|
Example:
|
|
517
|
-
>>> await fs.
|
|
459
|
+
>>> await fs.delete_file('/data/temp.txt')
|
|
518
460
|
"""
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
461
|
+
ino = await self._resolve_path(path)
|
|
462
|
+
if ino is None:
|
|
463
|
+
raise FileNotFoundError(f"ENOENT: no such file or directory, unlink '{path}'")
|
|
522
464
|
|
|
523
|
-
await
|
|
465
|
+
parent = await self._resolve_parent(path)
|
|
466
|
+
if not parent:
|
|
467
|
+
raise ValueError("Cannot delete root directory")
|
|
524
468
|
|
|
525
|
-
parent = await self._resolve_parent(normalized_path)
|
|
526
|
-
# parent is guaranteed to exist here since normalized_path != '/'
|
|
527
|
-
assert parent is not None
|
|
528
469
|
parent_ino, name = parent
|
|
529
470
|
|
|
530
471
|
# Delete the directory entry
|
|
@@ -536,12 +477,6 @@ class Filesystem:
|
|
|
536
477
|
(parent_ino, name),
|
|
537
478
|
)
|
|
538
479
|
|
|
539
|
-
# Decrement link count
|
|
540
|
-
await self._db.execute(
|
|
541
|
-
"UPDATE fs_inode SET nlink = nlink - 1 WHERE ino = ?",
|
|
542
|
-
(ino,),
|
|
543
|
-
)
|
|
544
|
-
|
|
545
480
|
# Check if this was the last link to the inode
|
|
546
481
|
link_count = await self._get_link_count(ino)
|
|
547
482
|
if link_count == 0:
|
|
@@ -553,18 +488,6 @@ class Filesystem:
|
|
|
553
488
|
|
|
554
489
|
await self._db.commit()
|
|
555
490
|
|
|
556
|
-
# Backwards-compatible alias
|
|
557
|
-
async def delete_file(self, path: str) -> None:
|
|
558
|
-
"""Delete a file (deprecated, use unlink instead)
|
|
559
|
-
|
|
560
|
-
Args:
|
|
561
|
-
path: Path to the file
|
|
562
|
-
|
|
563
|
-
Example:
|
|
564
|
-
>>> await fs.delete_file('/data/temp.txt')
|
|
565
|
-
"""
|
|
566
|
-
return await self.unlink(path)
|
|
567
|
-
|
|
568
491
|
async def stat(self, path: str) -> Stats:
|
|
569
492
|
"""Get file/directory statistics
|
|
570
493
|
|
|
@@ -579,11 +502,13 @@ class Filesystem:
|
|
|
579
502
|
>>> print(f"Size: {stats.size} bytes")
|
|
580
503
|
>>> print(f"Is file: {stats.is_file()}")
|
|
581
504
|
"""
|
|
582
|
-
|
|
505
|
+
ino = await self._resolve_path(path)
|
|
506
|
+
if ino is None:
|
|
507
|
+
raise FileNotFoundError(f"ENOENT: no such file or directory, stat '{path}'")
|
|
583
508
|
|
|
584
509
|
cursor = await self._db.execute(
|
|
585
510
|
"""
|
|
586
|
-
SELECT ino, mode,
|
|
511
|
+
SELECT ino, mode, uid, gid, size, atime, mtime, ctime
|
|
587
512
|
FROM fs_inode
|
|
588
513
|
WHERE ino = ?
|
|
589
514
|
""",
|
|
@@ -592,535 +517,18 @@ class Filesystem:
|
|
|
592
517
|
row = await cursor.fetchone()
|
|
593
518
|
|
|
594
519
|
if not row:
|
|
595
|
-
raise
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
path=normalized_path,
|
|
599
|
-
message="no such file or directory",
|
|
600
|
-
)
|
|
520
|
+
raise ValueError(f"Inode not found: {ino}")
|
|
521
|
+
|
|
522
|
+
nlink = await self._get_link_count(ino)
|
|
601
523
|
|
|
602
524
|
return Stats(
|
|
603
525
|
ino=row[0],
|
|
604
526
|
mode=row[1],
|
|
605
|
-
nlink=
|
|
606
|
-
uid=row[
|
|
607
|
-
gid=row[
|
|
608
|
-
size=row[
|
|
609
|
-
atime=row[
|
|
610
|
-
mtime=row[
|
|
611
|
-
ctime=row[
|
|
612
|
-
)
|
|
613
|
-
|
|
614
|
-
async def mkdir(self, path: str) -> None:
|
|
615
|
-
"""Create a directory (non-recursive)
|
|
616
|
-
|
|
617
|
-
Args:
|
|
618
|
-
path: Path to the directory to create
|
|
619
|
-
|
|
620
|
-
Example:
|
|
621
|
-
>>> await fs.mkdir('/data/new_dir')
|
|
622
|
-
"""
|
|
623
|
-
normalized_path = self._normalize_path(path)
|
|
624
|
-
|
|
625
|
-
existing = await self._resolve_path(normalized_path)
|
|
626
|
-
if existing is not None:
|
|
627
|
-
raise ErrnoException(
|
|
628
|
-
code="EEXIST",
|
|
629
|
-
syscall="mkdir",
|
|
630
|
-
path=normalized_path,
|
|
631
|
-
message="file already exists",
|
|
632
|
-
)
|
|
633
|
-
|
|
634
|
-
parent = await self._resolve_parent(normalized_path)
|
|
635
|
-
if not parent:
|
|
636
|
-
raise ErrnoException(
|
|
637
|
-
code="ENOENT",
|
|
638
|
-
syscall="mkdir",
|
|
639
|
-
path=normalized_path,
|
|
640
|
-
message="no such file or directory",
|
|
641
|
-
)
|
|
642
|
-
|
|
643
|
-
parent_ino, name = parent
|
|
644
|
-
await assert_inode_is_directory(self._db, parent_ino, "mkdir", normalized_path)
|
|
645
|
-
|
|
646
|
-
dir_ino = await self._create_inode(DEFAULT_DIR_MODE)
|
|
647
|
-
try:
|
|
648
|
-
await self._create_dentry(parent_ino, name, dir_ino)
|
|
649
|
-
except Exception:
|
|
650
|
-
raise ErrnoException(
|
|
651
|
-
code="EEXIST",
|
|
652
|
-
syscall="mkdir",
|
|
653
|
-
path=normalized_path,
|
|
654
|
-
message="file already exists",
|
|
655
|
-
)
|
|
656
|
-
|
|
657
|
-
async def rmdir(self, path: str) -> None:
|
|
658
|
-
"""Remove an empty directory
|
|
659
|
-
|
|
660
|
-
Args:
|
|
661
|
-
path: Path to the directory to remove
|
|
662
|
-
|
|
663
|
-
Example:
|
|
664
|
-
>>> await fs.rmdir('/data/empty_dir')
|
|
665
|
-
"""
|
|
666
|
-
normalized_path = self._normalize_path(path)
|
|
667
|
-
assert_not_root(normalized_path, "rmdir")
|
|
668
|
-
|
|
669
|
-
normalized_path, ino = await self._resolve_path_or_throw(normalized_path, "rmdir")
|
|
670
|
-
|
|
671
|
-
mode = await get_inode_mode_or_throw(self._db, ino, "rmdir", normalized_path)
|
|
672
|
-
assert_not_symlink_mode(mode, "rmdir", normalized_path)
|
|
673
|
-
if (mode & S_IFMT) != S_IFDIR:
|
|
674
|
-
raise ErrnoException(
|
|
675
|
-
code="ENOTDIR",
|
|
676
|
-
syscall="rmdir",
|
|
677
|
-
path=normalized_path,
|
|
678
|
-
message="not a directory",
|
|
679
|
-
)
|
|
680
|
-
|
|
681
|
-
cursor = await self._db.execute(
|
|
682
|
-
"""
|
|
683
|
-
SELECT 1 as one FROM fs_dentry
|
|
684
|
-
WHERE parent_ino = ?
|
|
685
|
-
LIMIT 1
|
|
686
|
-
""",
|
|
687
|
-
(ino,),
|
|
688
|
-
)
|
|
689
|
-
child = await cursor.fetchone()
|
|
690
|
-
if child:
|
|
691
|
-
raise ErrnoException(
|
|
692
|
-
code="ENOTEMPTY",
|
|
693
|
-
syscall="rmdir",
|
|
694
|
-
path=normalized_path,
|
|
695
|
-
message="directory not empty",
|
|
696
|
-
)
|
|
697
|
-
|
|
698
|
-
parent = await self._resolve_parent(normalized_path)
|
|
699
|
-
if not parent:
|
|
700
|
-
raise ErrnoException(
|
|
701
|
-
code="EPERM",
|
|
702
|
-
syscall="rmdir",
|
|
703
|
-
path=normalized_path,
|
|
704
|
-
message="operation not permitted",
|
|
705
|
-
)
|
|
706
|
-
|
|
707
|
-
parent_ino, name = parent
|
|
708
|
-
await self._remove_dentry_and_maybe_inode(parent_ino, name, ino)
|
|
709
|
-
|
|
710
|
-
async def rm(
|
|
711
|
-
self,
|
|
712
|
-
path: str,
|
|
713
|
-
force: bool = False,
|
|
714
|
-
recursive: bool = False,
|
|
715
|
-
) -> None:
|
|
716
|
-
"""Remove a file or directory
|
|
717
|
-
|
|
718
|
-
Args:
|
|
719
|
-
path: Path to remove
|
|
720
|
-
force: If True, ignore nonexistent files
|
|
721
|
-
recursive: If True, remove directories and their contents recursively
|
|
722
|
-
|
|
723
|
-
Example:
|
|
724
|
-
>>> await fs.rm('/data/file.txt')
|
|
725
|
-
>>> await fs.rm('/data/dir', recursive=True)
|
|
726
|
-
"""
|
|
727
|
-
normalized_path = self._normalize_path(path)
|
|
728
|
-
options = normalize_rm_options({"force": force, "recursive": recursive})
|
|
729
|
-
force = options["force"]
|
|
730
|
-
recursive = options["recursive"]
|
|
731
|
-
assert_not_root(normalized_path, "rm")
|
|
732
|
-
|
|
733
|
-
ino = await self._resolve_path(normalized_path)
|
|
734
|
-
if ino is None:
|
|
735
|
-
throw_enoent_unless_force(normalized_path, "rm", force)
|
|
736
|
-
return
|
|
737
|
-
|
|
738
|
-
mode = await get_inode_mode_or_throw(self._db, ino, "rm", normalized_path)
|
|
739
|
-
assert_not_symlink_mode(mode, "rm", normalized_path)
|
|
740
|
-
|
|
741
|
-
parent = await self._resolve_parent(normalized_path)
|
|
742
|
-
if not parent:
|
|
743
|
-
raise ErrnoException(
|
|
744
|
-
code="EPERM",
|
|
745
|
-
syscall="rm",
|
|
746
|
-
path=normalized_path,
|
|
747
|
-
message="operation not permitted",
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
parent_ino, name = parent
|
|
751
|
-
|
|
752
|
-
if (mode & S_IFMT) == S_IFDIR:
|
|
753
|
-
if not recursive:
|
|
754
|
-
raise ErrnoException(
|
|
755
|
-
code="EISDIR",
|
|
756
|
-
syscall="rm",
|
|
757
|
-
path=normalized_path,
|
|
758
|
-
message="illegal operation on a directory",
|
|
759
|
-
)
|
|
760
|
-
|
|
761
|
-
await self._rm_dir_contents_recursive(ino)
|
|
762
|
-
await self._remove_dentry_and_maybe_inode(parent_ino, name, ino)
|
|
763
|
-
return
|
|
764
|
-
|
|
765
|
-
# Regular file
|
|
766
|
-
await self._remove_dentry_and_maybe_inode(parent_ino, name, ino)
|
|
767
|
-
|
|
768
|
-
async def _rm_dir_contents_recursive(self, dir_ino: int) -> None:
|
|
769
|
-
"""Recursively remove directory contents"""
|
|
770
|
-
cursor = await self._db.execute(
|
|
771
|
-
"""
|
|
772
|
-
SELECT name, ino FROM fs_dentry
|
|
773
|
-
WHERE parent_ino = ?
|
|
774
|
-
ORDER BY name ASC
|
|
775
|
-
""",
|
|
776
|
-
(dir_ino,),
|
|
777
|
-
)
|
|
778
|
-
children = await cursor.fetchall()
|
|
779
|
-
|
|
780
|
-
for name, child_ino in children:
|
|
781
|
-
mode = await self._get_inode_mode(child_ino)
|
|
782
|
-
if mode is None:
|
|
783
|
-
# DB inconsistency; treat as already gone
|
|
784
|
-
continue
|
|
785
|
-
|
|
786
|
-
if (mode & S_IFMT) == S_IFDIR:
|
|
787
|
-
await self._rm_dir_contents_recursive(child_ino)
|
|
788
|
-
await self._remove_dentry_and_maybe_inode(dir_ino, name, child_ino)
|
|
789
|
-
else:
|
|
790
|
-
# Not supported yet (symlinks)
|
|
791
|
-
assert_not_symlink_mode(mode, "rm", "<symlink>")
|
|
792
|
-
await self._remove_dentry_and_maybe_inode(dir_ino, name, child_ino)
|
|
793
|
-
|
|
794
|
-
async def _remove_dentry_and_maybe_inode(self, parent_ino: int, name: str, ino: int) -> None:
|
|
795
|
-
"""Remove directory entry and inode if last link"""
|
|
796
|
-
await self._db.execute(
|
|
797
|
-
"""
|
|
798
|
-
DELETE FROM fs_dentry
|
|
799
|
-
WHERE parent_ino = ? AND name = ?
|
|
800
|
-
""",
|
|
801
|
-
(parent_ino, name),
|
|
802
|
-
)
|
|
803
|
-
|
|
804
|
-
# Decrement link count
|
|
805
|
-
await self._db.execute(
|
|
806
|
-
"UPDATE fs_inode SET nlink = nlink - 1 WHERE ino = ?",
|
|
807
|
-
(ino,),
|
|
527
|
+
nlink=nlink,
|
|
528
|
+
uid=row[2],
|
|
529
|
+
gid=row[3],
|
|
530
|
+
size=row[4],
|
|
531
|
+
atime=row[5],
|
|
532
|
+
mtime=row[6],
|
|
533
|
+
ctime=row[7],
|
|
808
534
|
)
|
|
809
|
-
|
|
810
|
-
link_count = await self._get_link_count(ino)
|
|
811
|
-
if link_count == 0:
|
|
812
|
-
await self._db.execute("DELETE FROM fs_inode WHERE ino = ?", (ino,))
|
|
813
|
-
await self._db.execute("DELETE FROM fs_data WHERE ino = ?", (ino,))
|
|
814
|
-
|
|
815
|
-
await self._db.commit()
|
|
816
|
-
|
|
817
|
-
async def rename(self, old_path: str, new_path: str) -> None:
|
|
818
|
-
"""Rename (move) a file or directory
|
|
819
|
-
|
|
820
|
-
Args:
|
|
821
|
-
old_path: Current path
|
|
822
|
-
new_path: New path
|
|
823
|
-
|
|
824
|
-
Example:
|
|
825
|
-
>>> await fs.rename('/data/old.txt', '/data/new.txt')
|
|
826
|
-
"""
|
|
827
|
-
old_normalized = self._normalize_path(old_path)
|
|
828
|
-
new_normalized = self._normalize_path(new_path)
|
|
829
|
-
|
|
830
|
-
# No-op
|
|
831
|
-
if old_normalized == new_normalized:
|
|
832
|
-
return
|
|
833
|
-
|
|
834
|
-
assert_not_root(old_normalized, "rename")
|
|
835
|
-
assert_not_root(new_normalized, "rename")
|
|
836
|
-
|
|
837
|
-
old_parent = await self._resolve_parent(old_normalized)
|
|
838
|
-
if not old_parent:
|
|
839
|
-
raise ErrnoException(
|
|
840
|
-
code="EPERM",
|
|
841
|
-
syscall="rename",
|
|
842
|
-
path=old_normalized,
|
|
843
|
-
message="operation not permitted",
|
|
844
|
-
)
|
|
845
|
-
|
|
846
|
-
new_parent = await self._resolve_parent(new_normalized)
|
|
847
|
-
if not new_parent:
|
|
848
|
-
raise ErrnoException(
|
|
849
|
-
code="ENOENT",
|
|
850
|
-
syscall="rename",
|
|
851
|
-
path=new_normalized,
|
|
852
|
-
message="no such file or directory",
|
|
853
|
-
)
|
|
854
|
-
|
|
855
|
-
new_parent_ino, new_name = new_parent
|
|
856
|
-
|
|
857
|
-
# Ensure destination parent exists and is a directory
|
|
858
|
-
await assert_inode_is_directory(self._db, new_parent_ino, "rename", new_normalized)
|
|
859
|
-
|
|
860
|
-
# Begin transaction
|
|
861
|
-
# Note: turso.aio doesn't support explicit BEGIN, but execute should be atomic
|
|
862
|
-
try:
|
|
863
|
-
old_normalized, old_ino = await self._resolve_path_or_throw(old_normalized, "rename")
|
|
864
|
-
old_mode = await get_inode_mode_or_throw(self._db, old_ino, "rename", old_normalized)
|
|
865
|
-
assert_not_symlink_mode(old_mode, "rename", old_normalized)
|
|
866
|
-
old_is_dir = (old_mode & S_IFMT) == S_IFDIR
|
|
867
|
-
|
|
868
|
-
# Prevent renaming a directory into its own subtree (would create cycles)
|
|
869
|
-
if old_is_dir and new_normalized.startswith(old_normalized + "/"):
|
|
870
|
-
raise ErrnoException(
|
|
871
|
-
code="EINVAL",
|
|
872
|
-
syscall="rename",
|
|
873
|
-
path=new_normalized,
|
|
874
|
-
message="invalid argument",
|
|
875
|
-
)
|
|
876
|
-
|
|
877
|
-
new_ino = await self._resolve_path(new_normalized)
|
|
878
|
-
if new_ino is not None:
|
|
879
|
-
new_mode = await get_inode_mode_or_throw(
|
|
880
|
-
self._db, new_ino, "rename", new_normalized
|
|
881
|
-
)
|
|
882
|
-
assert_not_symlink_mode(new_mode, "rename", new_normalized)
|
|
883
|
-
new_is_dir = (new_mode & S_IFMT) == S_IFDIR
|
|
884
|
-
|
|
885
|
-
if new_is_dir and not old_is_dir:
|
|
886
|
-
raise ErrnoException(
|
|
887
|
-
code="EISDIR",
|
|
888
|
-
syscall="rename",
|
|
889
|
-
path=new_normalized,
|
|
890
|
-
message="illegal operation on a directory",
|
|
891
|
-
)
|
|
892
|
-
if not new_is_dir and old_is_dir:
|
|
893
|
-
raise ErrnoException(
|
|
894
|
-
code="ENOTDIR",
|
|
895
|
-
syscall="rename",
|
|
896
|
-
path=new_normalized,
|
|
897
|
-
message="not a directory",
|
|
898
|
-
)
|
|
899
|
-
|
|
900
|
-
# If replacing a directory, it must be empty
|
|
901
|
-
if new_is_dir:
|
|
902
|
-
cursor = await self._db.execute(
|
|
903
|
-
"""
|
|
904
|
-
SELECT 1 as one FROM fs_dentry
|
|
905
|
-
WHERE parent_ino = ?
|
|
906
|
-
LIMIT 1
|
|
907
|
-
""",
|
|
908
|
-
(new_ino,),
|
|
909
|
-
)
|
|
910
|
-
child = await cursor.fetchone()
|
|
911
|
-
if child:
|
|
912
|
-
raise ErrnoException(
|
|
913
|
-
code="ENOTEMPTY",
|
|
914
|
-
syscall="rename",
|
|
915
|
-
path=new_normalized,
|
|
916
|
-
message="directory not empty",
|
|
917
|
-
)
|
|
918
|
-
|
|
919
|
-
# Remove the destination entry (and inode if this was the last link)
|
|
920
|
-
await self._remove_dentry_and_maybe_inode(new_parent_ino, new_name, new_ino)
|
|
921
|
-
|
|
922
|
-
# Move the directory entry
|
|
923
|
-
old_parent_ino, old_name = old_parent
|
|
924
|
-
await self._db.execute(
|
|
925
|
-
"""
|
|
926
|
-
UPDATE fs_dentry
|
|
927
|
-
SET parent_ino = ?, name = ?
|
|
928
|
-
WHERE parent_ino = ? AND name = ?
|
|
929
|
-
""",
|
|
930
|
-
(new_parent_ino, new_name, old_parent_ino, old_name),
|
|
931
|
-
)
|
|
932
|
-
|
|
933
|
-
# Update timestamps
|
|
934
|
-
now = int(time.time())
|
|
935
|
-
await self._db.execute(
|
|
936
|
-
"""
|
|
937
|
-
UPDATE fs_inode
|
|
938
|
-
SET ctime = ?
|
|
939
|
-
WHERE ino = ?
|
|
940
|
-
""",
|
|
941
|
-
(now, old_ino),
|
|
942
|
-
)
|
|
943
|
-
|
|
944
|
-
await self._db.execute(
|
|
945
|
-
"""
|
|
946
|
-
UPDATE fs_inode
|
|
947
|
-
SET mtime = ?, ctime = ?
|
|
948
|
-
WHERE ino = ?
|
|
949
|
-
""",
|
|
950
|
-
(now, now, old_parent_ino),
|
|
951
|
-
)
|
|
952
|
-
if new_parent_ino != old_parent_ino:
|
|
953
|
-
await self._db.execute(
|
|
954
|
-
"""
|
|
955
|
-
UPDATE fs_inode
|
|
956
|
-
SET mtime = ?, ctime = ?
|
|
957
|
-
WHERE ino = ?
|
|
958
|
-
""",
|
|
959
|
-
(now, now, new_parent_ino),
|
|
960
|
-
)
|
|
961
|
-
|
|
962
|
-
await self._db.commit()
|
|
963
|
-
except Exception:
|
|
964
|
-
# turso.aio doesn't have explicit rollback, changes are rolled back automatically
|
|
965
|
-
raise
|
|
966
|
-
|
|
967
|
-
async def copy_file(self, src: str, dest: str) -> None:
|
|
968
|
-
"""Copy a file. Overwrites destination if it exists.
|
|
969
|
-
|
|
970
|
-
Args:
|
|
971
|
-
src: Source file path
|
|
972
|
-
dest: Destination file path
|
|
973
|
-
|
|
974
|
-
Example:
|
|
975
|
-
>>> await fs.copy_file('/data/src.txt', '/data/dest.txt')
|
|
976
|
-
"""
|
|
977
|
-
src_normalized = self._normalize_path(src)
|
|
978
|
-
dest_normalized = self._normalize_path(dest)
|
|
979
|
-
|
|
980
|
-
if src_normalized == dest_normalized:
|
|
981
|
-
raise ErrnoException(
|
|
982
|
-
code="EINVAL",
|
|
983
|
-
syscall="copyfile",
|
|
984
|
-
path=dest_normalized,
|
|
985
|
-
message="invalid argument",
|
|
986
|
-
)
|
|
987
|
-
|
|
988
|
-
# Resolve and validate source
|
|
989
|
-
src_normalized, src_ino = await self._resolve_path_or_throw(src_normalized, "copyfile")
|
|
990
|
-
await assert_readable_existing_inode(self._db, src_ino, "copyfile", src_normalized)
|
|
991
|
-
|
|
992
|
-
cursor = await self._db.execute(
|
|
993
|
-
"""
|
|
994
|
-
SELECT mode, uid, gid, size FROM fs_inode WHERE ino = ?
|
|
995
|
-
""",
|
|
996
|
-
(src_ino,),
|
|
997
|
-
)
|
|
998
|
-
src_row = await cursor.fetchone()
|
|
999
|
-
if not src_row:
|
|
1000
|
-
raise ErrnoException(
|
|
1001
|
-
code="ENOENT",
|
|
1002
|
-
syscall="copyfile",
|
|
1003
|
-
path=src_normalized,
|
|
1004
|
-
message="no such file or directory",
|
|
1005
|
-
)
|
|
1006
|
-
|
|
1007
|
-
src_mode, src_uid, src_gid, src_size = src_row
|
|
1008
|
-
|
|
1009
|
-
# Destination parent must exist and be a directory
|
|
1010
|
-
dest_parent = await self._resolve_parent(dest_normalized)
|
|
1011
|
-
if not dest_parent:
|
|
1012
|
-
raise ErrnoException(
|
|
1013
|
-
code="ENOENT",
|
|
1014
|
-
syscall="copyfile",
|
|
1015
|
-
path=dest_normalized,
|
|
1016
|
-
message="no such file or directory",
|
|
1017
|
-
)
|
|
1018
|
-
|
|
1019
|
-
dest_parent_ino, dest_name = dest_parent
|
|
1020
|
-
await assert_inode_is_directory(self._db, dest_parent_ino, "copyfile", dest_normalized)
|
|
1021
|
-
|
|
1022
|
-
try:
|
|
1023
|
-
now = int(time.time())
|
|
1024
|
-
|
|
1025
|
-
# If destination exists, it must be a file (overwrite semantics)
|
|
1026
|
-
dest_ino = await self._resolve_path(dest_normalized)
|
|
1027
|
-
if dest_ino is not None:
|
|
1028
|
-
dest_mode = await get_inode_mode_or_throw(
|
|
1029
|
-
self._db, dest_ino, "copyfile", dest_normalized
|
|
1030
|
-
)
|
|
1031
|
-
assert_not_symlink_mode(dest_mode, "copyfile", dest_normalized)
|
|
1032
|
-
if (dest_mode & S_IFMT) == S_IFDIR:
|
|
1033
|
-
raise ErrnoException(
|
|
1034
|
-
code="EISDIR",
|
|
1035
|
-
syscall="copyfile",
|
|
1036
|
-
path=dest_normalized,
|
|
1037
|
-
message="illegal operation on a directory",
|
|
1038
|
-
)
|
|
1039
|
-
|
|
1040
|
-
# Replace destination contents
|
|
1041
|
-
await self._db.execute("DELETE FROM fs_data WHERE ino = ?", (dest_ino,))
|
|
1042
|
-
await self._db.commit()
|
|
1043
|
-
|
|
1044
|
-
# Copy data chunks
|
|
1045
|
-
cursor = await self._db.execute(
|
|
1046
|
-
"""
|
|
1047
|
-
SELECT chunk_index, data FROM fs_data
|
|
1048
|
-
WHERE ino = ?
|
|
1049
|
-
ORDER BY chunk_index ASC
|
|
1050
|
-
""",
|
|
1051
|
-
(src_ino,),
|
|
1052
|
-
)
|
|
1053
|
-
src_chunks = await cursor.fetchall()
|
|
1054
|
-
for chunk_index, data in src_chunks:
|
|
1055
|
-
await self._db.execute(
|
|
1056
|
-
"""
|
|
1057
|
-
INSERT INTO fs_data (ino, chunk_index, data)
|
|
1058
|
-
VALUES (?, ?, ?)
|
|
1059
|
-
""",
|
|
1060
|
-
(dest_ino, chunk_index, data),
|
|
1061
|
-
)
|
|
1062
|
-
|
|
1063
|
-
await self._db.execute(
|
|
1064
|
-
"""
|
|
1065
|
-
UPDATE fs_inode
|
|
1066
|
-
SET mode = ?, uid = ?, gid = ?, size = ?, mtime = ?, ctime = ?
|
|
1067
|
-
WHERE ino = ?
|
|
1068
|
-
""",
|
|
1069
|
-
(src_mode, src_uid, src_gid, src_size, now, now, dest_ino),
|
|
1070
|
-
)
|
|
1071
|
-
else:
|
|
1072
|
-
# Create new destination inode + dentry
|
|
1073
|
-
dest_ino_created = await self._create_inode(src_mode, src_uid, src_gid)
|
|
1074
|
-
await self._create_dentry(dest_parent_ino, dest_name, dest_ino_created)
|
|
1075
|
-
|
|
1076
|
-
# Copy data chunks
|
|
1077
|
-
cursor = await self._db.execute(
|
|
1078
|
-
"""
|
|
1079
|
-
SELECT chunk_index, data FROM fs_data
|
|
1080
|
-
WHERE ino = ?
|
|
1081
|
-
ORDER BY chunk_index ASC
|
|
1082
|
-
""",
|
|
1083
|
-
(src_ino,),
|
|
1084
|
-
)
|
|
1085
|
-
src_chunks = await cursor.fetchall()
|
|
1086
|
-
for chunk_index, data in src_chunks:
|
|
1087
|
-
await self._db.execute(
|
|
1088
|
-
"""
|
|
1089
|
-
INSERT INTO fs_data (ino, chunk_index, data)
|
|
1090
|
-
VALUES (?, ?, ?)
|
|
1091
|
-
""",
|
|
1092
|
-
(dest_ino_created, chunk_index, data),
|
|
1093
|
-
)
|
|
1094
|
-
|
|
1095
|
-
await self._db.execute(
|
|
1096
|
-
"""
|
|
1097
|
-
UPDATE fs_inode
|
|
1098
|
-
SET size = ?, mtime = ?, ctime = ?
|
|
1099
|
-
WHERE ino = ?
|
|
1100
|
-
""",
|
|
1101
|
-
(src_size, now, now, dest_ino_created),
|
|
1102
|
-
)
|
|
1103
|
-
|
|
1104
|
-
await self._db.commit()
|
|
1105
|
-
except Exception:
|
|
1106
|
-
raise
|
|
1107
|
-
|
|
1108
|
-
async def access(self, path: str) -> None:
|
|
1109
|
-
"""Test a user's permissions for the file or directory.
|
|
1110
|
-
Currently supports existence checks only (F_OK semantics).
|
|
1111
|
-
|
|
1112
|
-
Args:
|
|
1113
|
-
path: Path to check
|
|
1114
|
-
|
|
1115
|
-
Example:
|
|
1116
|
-
>>> await fs.access('/data/config.json')
|
|
1117
|
-
"""
|
|
1118
|
-
normalized_path = self._normalize_path(path)
|
|
1119
|
-
ino = await self._resolve_path(normalized_path)
|
|
1120
|
-
if ino is None:
|
|
1121
|
-
raise ErrnoException(
|
|
1122
|
-
code="ENOENT",
|
|
1123
|
-
syscall="access",
|
|
1124
|
-
path=normalized_path,
|
|
1125
|
-
message="no such file or directory",
|
|
1126
|
-
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentfs-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.0rc2
|
|
4
4
|
Summary: AgentFS Python SDK - A filesystem and key-value store for AI agents
|
|
5
5
|
Author: Turso
|
|
6
6
|
License: MIT
|
|
@@ -16,11 +16,9 @@ Classifier: Operating System :: POSIX :: Linux
|
|
|
16
16
|
Classifier: Operating System :: Microsoft :: Windows
|
|
17
17
|
Classifier: Operating System :: MacOS
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
21
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.12
|
|
24
22
|
Description-Content-Type: text/markdown
|
|
25
23
|
Requires-Dist: pyturso==0.4.0rc17
|
|
26
24
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
agentfs_sdk/__init__.py,sha256=K7crr3djIXWS4sXWzVMZH7HS4R5jYVEQqs_n72mderQ,450
|
|
2
|
+
agentfs_sdk/agentfs.py,sha256=WW7-foT9Vqeq8ZVAdixxHCQHq74zkaYhPM0xVpZL_WM,4071
|
|
3
|
+
agentfs_sdk/filesystem.py,sha256=Gr80Jy36kILb-Nvd2SBFmVmsDdCQzdN_aGCneHMlros,16462
|
|
4
|
+
agentfs_sdk/kvstore.py,sha256=3p3UeuhcJUDId6bCYMAUZ1jIZqyhMMaE0BjlnV9cjAI,4048
|
|
5
|
+
agentfs_sdk/toolcalls.py,sha256=OEEywUiPO8dPs263Xiu1CfeaQzYq-E6W3biu3nnBQoU,13041
|
|
6
|
+
agentfs_sdk-0.4.0rc2.dist-info/METADATA,sha256=FaLTSL10V97gjBZVQ6jN-p4z45fAa4ROwoNvKxOUxoA,5216
|
|
7
|
+
agentfs_sdk-0.4.0rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
agentfs_sdk-0.4.0rc2.dist-info/top_level.txt,sha256=yAslKFmXq_LQAnRhDcE5uj-KPuHdfyUGB4EzlQLmsuI,12
|
|
9
|
+
agentfs_sdk-0.4.0rc2.dist-info/RECORD,,
|
agentfs_sdk/constants.py
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"""Filesystem constants"""
|
|
2
|
-
|
|
3
|
-
# File types for mode field
|
|
4
|
-
S_IFMT = 0o170000 # File type mask
|
|
5
|
-
S_IFREG = 0o100000 # Regular file
|
|
6
|
-
S_IFDIR = 0o040000 # Directory
|
|
7
|
-
S_IFLNK = 0o120000 # Symbolic link
|
|
8
|
-
|
|
9
|
-
# Default permissions
|
|
10
|
-
DEFAULT_FILE_MODE = S_IFREG | 0o644 # Regular file, rw-r--r--
|
|
11
|
-
DEFAULT_DIR_MODE = S_IFDIR | 0o755 # Directory, rwxr-xr-x
|
|
12
|
-
|
|
13
|
-
DEFAULT_CHUNK_SIZE = 4096
|
agentfs_sdk/errors.py
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"""Error types for filesystem operations"""
|
|
2
|
-
|
|
3
|
-
from typing import Literal, Optional
|
|
4
|
-
|
|
5
|
-
# POSIX-style error codes for filesystem operations
|
|
6
|
-
FsErrorCode = Literal[
|
|
7
|
-
"ENOENT", # No such file or directory
|
|
8
|
-
"EEXIST", # File already exists
|
|
9
|
-
"EISDIR", # Is a directory (when file expected)
|
|
10
|
-
"ENOTDIR", # Not a directory (when directory expected)
|
|
11
|
-
"ENOTEMPTY", # Directory not empty
|
|
12
|
-
"EPERM", # Operation not permitted
|
|
13
|
-
"EINVAL", # Invalid argument
|
|
14
|
-
"ENOSYS", # Function not implemented (use for symlinks)
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
# Filesystem syscall names for error reporting
|
|
18
|
-
# rm, scandir and copyfile are not actual syscalls but used for convenience
|
|
19
|
-
FsSyscall = Literal[
|
|
20
|
-
"open",
|
|
21
|
-
"stat",
|
|
22
|
-
"mkdir",
|
|
23
|
-
"rmdir",
|
|
24
|
-
"rm",
|
|
25
|
-
"unlink",
|
|
26
|
-
"rename",
|
|
27
|
-
"scandir",
|
|
28
|
-
"copyfile",
|
|
29
|
-
"access",
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class ErrnoException(Exception):
|
|
34
|
-
"""Exception with errno-style attributes
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
code: POSIX error code (e.g., 'ENOENT')
|
|
38
|
-
syscall: System call name (e.g., 'open')
|
|
39
|
-
path: Optional path involved in the error
|
|
40
|
-
message: Optional custom message (defaults to code)
|
|
41
|
-
|
|
42
|
-
Example:
|
|
43
|
-
>>> raise ErrnoException('ENOENT', 'open', '/missing.txt')
|
|
44
|
-
ErrnoException: ENOENT: no such file or directory, open '/missing.txt'
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
def __init__(
|
|
48
|
-
self,
|
|
49
|
-
code: FsErrorCode,
|
|
50
|
-
syscall: FsSyscall,
|
|
51
|
-
path: Optional[str] = None,
|
|
52
|
-
message: Optional[str] = None,
|
|
53
|
-
):
|
|
54
|
-
base = message if message else code
|
|
55
|
-
suffix = f" '{path}'" if path is not None else ""
|
|
56
|
-
error_message = f"{code}: {base}, {syscall}{suffix}"
|
|
57
|
-
super().__init__(error_message)
|
|
58
|
-
self.code = code
|
|
59
|
-
self.syscall = syscall
|
|
60
|
-
self.path = path
|
agentfs_sdk/guards.py
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"""Guard functions for filesystem operations validation"""
|
|
2
|
-
|
|
3
|
-
from typing import Any, Dict, Optional
|
|
4
|
-
|
|
5
|
-
from turso.aio import Connection
|
|
6
|
-
|
|
7
|
-
from .constants import S_IFDIR, S_IFLNK, S_IFMT
|
|
8
|
-
from .errors import ErrnoException, FsSyscall
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
async def _get_inode_mode(db: Connection, ino: int) -> Optional[int]:
|
|
12
|
-
"""Get mode for an inode"""
|
|
13
|
-
cursor = await db.execute("SELECT mode FROM fs_inode WHERE ino = ?", (ino,))
|
|
14
|
-
row = await cursor.fetchone()
|
|
15
|
-
return row[0] if row else None
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _is_dir_mode(mode: int) -> bool:
|
|
19
|
-
"""Check if mode represents a directory"""
|
|
20
|
-
return (mode & S_IFMT) == S_IFDIR
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
async def get_inode_mode_or_throw(
|
|
24
|
-
db: Connection,
|
|
25
|
-
ino: int,
|
|
26
|
-
syscall: FsSyscall,
|
|
27
|
-
path: str,
|
|
28
|
-
) -> int:
|
|
29
|
-
"""Get inode mode or throw ENOENT if not found"""
|
|
30
|
-
mode = await _get_inode_mode(db, ino)
|
|
31
|
-
if mode is None:
|
|
32
|
-
raise ErrnoException(
|
|
33
|
-
code="ENOENT",
|
|
34
|
-
syscall=syscall,
|
|
35
|
-
path=path,
|
|
36
|
-
message="no such file or directory",
|
|
37
|
-
)
|
|
38
|
-
return mode
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def assert_not_root(path: str, syscall: FsSyscall) -> None:
|
|
42
|
-
"""Assert that path is not root directory"""
|
|
43
|
-
if path == "/":
|
|
44
|
-
raise ErrnoException(
|
|
45
|
-
code="EPERM",
|
|
46
|
-
syscall=syscall,
|
|
47
|
-
path=path,
|
|
48
|
-
message="operation not permitted on root directory",
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def normalize_rm_options(options: Optional[Dict[str, Any]]) -> Dict[str, bool]:
|
|
53
|
-
"""Normalize rm options to ensure force and recursive are booleans"""
|
|
54
|
-
return {
|
|
55
|
-
"force": options.get("force", False) if options else False,
|
|
56
|
-
"recursive": options.get("recursive", False) if options else False,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def throw_enoent_unless_force(path: str, syscall: FsSyscall, force: bool) -> None:
|
|
61
|
-
"""Throw ENOENT unless force flag is set"""
|
|
62
|
-
if force:
|
|
63
|
-
return
|
|
64
|
-
raise ErrnoException(
|
|
65
|
-
code="ENOENT",
|
|
66
|
-
syscall=syscall,
|
|
67
|
-
path=path,
|
|
68
|
-
message="no such file or directory",
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def assert_not_symlink_mode(mode: int, syscall: FsSyscall, path: str) -> None:
|
|
73
|
-
"""Assert that mode does not represent a symlink"""
|
|
74
|
-
if (mode & S_IFMT) == S_IFLNK:
|
|
75
|
-
raise ErrnoException(
|
|
76
|
-
code="ENOSYS",
|
|
77
|
-
syscall=syscall,
|
|
78
|
-
path=path,
|
|
79
|
-
message="symbolic links not supported yet",
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
async def _assert_existing_non_dir_non_symlink_inode(
|
|
84
|
-
db: Connection,
|
|
85
|
-
ino: int,
|
|
86
|
-
syscall: FsSyscall,
|
|
87
|
-
full_path_for_error: str,
|
|
88
|
-
) -> None:
|
|
89
|
-
"""Assert inode exists and is neither directory nor symlink"""
|
|
90
|
-
mode = await _get_inode_mode(db, ino)
|
|
91
|
-
if mode is None:
|
|
92
|
-
raise ErrnoException(
|
|
93
|
-
code="ENOENT",
|
|
94
|
-
syscall=syscall,
|
|
95
|
-
path=full_path_for_error,
|
|
96
|
-
message="no such file or directory",
|
|
97
|
-
)
|
|
98
|
-
if _is_dir_mode(mode):
|
|
99
|
-
raise ErrnoException(
|
|
100
|
-
code="EISDIR",
|
|
101
|
-
syscall=syscall,
|
|
102
|
-
path=full_path_for_error,
|
|
103
|
-
message="illegal operation on a directory",
|
|
104
|
-
)
|
|
105
|
-
assert_not_symlink_mode(mode, syscall, full_path_for_error)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
async def assert_inode_is_directory(
|
|
109
|
-
db: Connection,
|
|
110
|
-
ino: int,
|
|
111
|
-
syscall: FsSyscall,
|
|
112
|
-
full_path_for_error: str,
|
|
113
|
-
) -> None:
|
|
114
|
-
"""Assert that inode is a directory"""
|
|
115
|
-
mode = await _get_inode_mode(db, ino)
|
|
116
|
-
if mode is None:
|
|
117
|
-
raise ErrnoException(
|
|
118
|
-
code="ENOENT",
|
|
119
|
-
syscall=syscall,
|
|
120
|
-
path=full_path_for_error,
|
|
121
|
-
message="no such file or directory",
|
|
122
|
-
)
|
|
123
|
-
if not _is_dir_mode(mode):
|
|
124
|
-
raise ErrnoException(
|
|
125
|
-
code="ENOTDIR",
|
|
126
|
-
syscall=syscall,
|
|
127
|
-
path=full_path_for_error,
|
|
128
|
-
message="not a directory",
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
async def assert_writable_existing_inode(
|
|
133
|
-
db: Connection,
|
|
134
|
-
ino: int,
|
|
135
|
-
syscall: FsSyscall,
|
|
136
|
-
full_path_for_error: str,
|
|
137
|
-
) -> None:
|
|
138
|
-
"""Assert inode is writable (exists and is not directory/symlink)"""
|
|
139
|
-
await _assert_existing_non_dir_non_symlink_inode(db, ino, syscall, full_path_for_error)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
async def assert_readable_existing_inode(
|
|
143
|
-
db: Connection,
|
|
144
|
-
ino: int,
|
|
145
|
-
syscall: FsSyscall,
|
|
146
|
-
full_path_for_error: str,
|
|
147
|
-
) -> None:
|
|
148
|
-
"""Assert inode is readable (exists and is not directory/symlink)"""
|
|
149
|
-
await _assert_existing_non_dir_non_symlink_inode(db, ino, syscall, full_path_for_error)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
async def assert_readdir_target_inode(
|
|
153
|
-
db: Connection,
|
|
154
|
-
ino: int,
|
|
155
|
-
full_path_for_error: str,
|
|
156
|
-
) -> None:
|
|
157
|
-
"""Assert inode is a valid readdir target (directory, not symlink)"""
|
|
158
|
-
syscall: FsSyscall = "scandir"
|
|
159
|
-
mode = await _get_inode_mode(db, ino)
|
|
160
|
-
if mode is None:
|
|
161
|
-
raise ErrnoException(
|
|
162
|
-
code="ENOENT",
|
|
163
|
-
syscall=syscall,
|
|
164
|
-
path=full_path_for_error,
|
|
165
|
-
message="no such file or directory",
|
|
166
|
-
)
|
|
167
|
-
assert_not_symlink_mode(mode, syscall, full_path_for_error)
|
|
168
|
-
if not _is_dir_mode(mode):
|
|
169
|
-
raise ErrnoException(
|
|
170
|
-
code="ENOTDIR",
|
|
171
|
-
syscall=syscall,
|
|
172
|
-
path=full_path_for_error,
|
|
173
|
-
message="not a directory",
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
async def assert_unlink_target_inode(
|
|
178
|
-
db: Connection,
|
|
179
|
-
ino: int,
|
|
180
|
-
full_path_for_error: str,
|
|
181
|
-
) -> None:
|
|
182
|
-
"""Assert inode is a valid unlink target (file, not directory/symlink)"""
|
|
183
|
-
syscall: FsSyscall = "unlink"
|
|
184
|
-
mode = await _get_inode_mode(db, ino)
|
|
185
|
-
if mode is None:
|
|
186
|
-
raise ErrnoException(
|
|
187
|
-
code="ENOENT",
|
|
188
|
-
syscall=syscall,
|
|
189
|
-
path=full_path_for_error,
|
|
190
|
-
message="no such file or directory",
|
|
191
|
-
)
|
|
192
|
-
if _is_dir_mode(mode):
|
|
193
|
-
raise ErrnoException(
|
|
194
|
-
code="EISDIR",
|
|
195
|
-
syscall=syscall,
|
|
196
|
-
path=full_path_for_error,
|
|
197
|
-
message="illegal operation on a directory",
|
|
198
|
-
)
|
|
199
|
-
assert_not_symlink_mode(mode, syscall, full_path_for_error)
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
agentfs_sdk/__init__.py,sha256=Z5Sgyy1FiWoRnX6QIZn5xCNpEXbVdAYKDdJNnNUbl_M,655
|
|
2
|
-
agentfs_sdk/agentfs.py,sha256=WW7-foT9Vqeq8ZVAdixxHCQHq74zkaYhPM0xVpZL_WM,4071
|
|
3
|
-
agentfs_sdk/constants.py,sha256=wKBX0PwtgGo1lOz61XHouje9nCFEok2AooHWVt7QJrI,367
|
|
4
|
-
agentfs_sdk/errors.py,sha256=P37aeKn8MJ6cj5ZLjUFd8vMBj5m4cUrqmc-4pvK-d3g,1728
|
|
5
|
-
agentfs_sdk/filesystem.py,sha256=TVFpftptXLWWzQjzuq2L3Bbi9sIAjDGwspcBdxVyGNU,37190
|
|
6
|
-
agentfs_sdk/guards.py,sha256=Hea6FnJMn8vHY5-EwCgX4uZBA4TNT4t90br_Q9VIYs0,5804
|
|
7
|
-
agentfs_sdk/kvstore.py,sha256=3p3UeuhcJUDId6bCYMAUZ1jIZqyhMMaE0BjlnV9cjAI,4048
|
|
8
|
-
agentfs_sdk/toolcalls.py,sha256=OEEywUiPO8dPs263Xiu1CfeaQzYq-E6W3biu3nnBQoU,13041
|
|
9
|
-
agentfs_sdk-0.4.0.dist-info/METADATA,sha256=Yw5H--FB5fccugtZRFA7S6dHgIOlHunuE3OwrS1NU_Y,5315
|
|
10
|
-
agentfs_sdk-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
-
agentfs_sdk-0.4.0.dist-info/top_level.txt,sha256=yAslKFmXq_LQAnRhDcE5uj-KPuHdfyUGB4EzlQLmsuI,12
|
|
12
|
-
agentfs_sdk-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|