unique-sdk 2026.42.0.dev3__tar.gz → 2026.42.0.dev4__tar.gz
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.
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/PKG-INFO +1 -1
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/pyproject.toml +2 -1
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_content.py +1 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/files.py +11 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/folders.py +11 -2
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/navigation.py +20 -2
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/read.py +3 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/search.py +1 -0
- unique_sdk-2026.42.0.dev4/unique_sdk/cli/skill_guard.py +314 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +1 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/state.py +42 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/README.md +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/__init__.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_api_requestor.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_api_resource.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_api_version.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_error.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_http_client.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_list_object.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_object_classes.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_request_options.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_unique_object.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_unique_ql.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_unique_response.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_util.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_version.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/_webhook.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/__init__.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_acronyms.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_agentic_table.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_analytics_order.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_benchmarking.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_briefing.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_chat_completion.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_context_memory.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_elicitation.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_embedding.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_event.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_folder.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_group.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_integrated.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_llm_models.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_mcp.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_message.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_message_assessment.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_message_execution.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_message_log.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_message_tool.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_module.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_scheduled_task.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_search.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_search_string.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_short_term_memory.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_space.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_user.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_web_search.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/__init__.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/__main__.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/cli.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/__init__.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/agentic_table.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/browser.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/cite_file.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/elicitation.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/mcp.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/subagent.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/web_search.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/web_search_config.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/config.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/formatting.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/identity.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/metadata_filter.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/shell.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/workspace.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/utils/analytics_order_run.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/utils/benchmarking_run.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/utils/chat_history.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/utils/chat_in_space.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/utils/file_io.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/utils/sources.py +0 -0
- {unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/utils/token.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-sdk
|
|
3
|
-
Version: 2026.42.0.
|
|
3
|
+
Version: 2026.42.0.dev4
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Martin Fadler, Konstantin Krauss, Andreas Hauri
|
|
6
6
|
Author-email: Martin Fadler <martin.fadler@unique.ch>, Konstantin Krauss <konstantin@unique.ch>, Andreas Hauri <andreas@unique.ch>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique_sdk"
|
|
3
|
-
version = "2026.42.0.
|
|
3
|
+
version = "2026.42.0.dev4"
|
|
4
4
|
description = ""
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -91,6 +91,7 @@ markers = [
|
|
|
91
91
|
"asyncio: asyncio tests",
|
|
92
92
|
"integration: integration tests that require API access or credentials",
|
|
93
93
|
"serial: tests that must run serially",
|
|
94
|
+
"skill_guard: CLI tests that exercise the real skill guard",
|
|
94
95
|
"unit: unit tests",
|
|
95
96
|
"verified: AI-generated tests with human verification",
|
|
96
97
|
]
|
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
import unique_sdk
|
|
13
13
|
from unique_sdk.cli.formatting import format_content_info
|
|
14
|
+
from unique_sdk.cli.skill_guard import skill_denial
|
|
14
15
|
from unique_sdk.cli.state import ShellState
|
|
15
16
|
from unique_sdk.utils.file_io import download_content, upload_file
|
|
16
17
|
|
|
@@ -337,6 +338,10 @@ def cmd_upload(
|
|
|
337
338
|
"upload: permission denied: destination is outside your "
|
|
338
339
|
f"task scope ({state.scope_denial_hint()})."
|
|
339
340
|
)
|
|
341
|
+
if state.skill_guard.is_folder_write_denied(
|
|
342
|
+
scope_id, new_file_name=display_name
|
|
343
|
+
):
|
|
344
|
+
return skill_denial("upload", display_name)
|
|
340
345
|
|
|
341
346
|
mime_type = _detect_upload_mime_type(path)
|
|
342
347
|
|
|
@@ -426,6 +431,8 @@ def cmd_download(
|
|
|
426
431
|
"""Download a file by name or content ID."""
|
|
427
432
|
try:
|
|
428
433
|
content_id, display_name = _resolve_content_id(state, name_or_id)
|
|
434
|
+
if state.is_skill_content_read_denied(content_id):
|
|
435
|
+
return skill_denial("download", display_name)
|
|
429
436
|
|
|
430
437
|
raw_name = (
|
|
431
438
|
display_name if not display_name.startswith("cont_") else f"{content_id}"
|
|
@@ -462,6 +469,8 @@ def cmd_rm(state: ShellState, name_or_id: str) -> str:
|
|
|
462
469
|
content_id, display_name = _resolve_content_id(
|
|
463
470
|
state, name_or_id, allow_chat_files=False
|
|
464
471
|
)
|
|
472
|
+
if state.is_skill_content_write_denied(content_id):
|
|
473
|
+
return skill_denial("rm", display_name)
|
|
465
474
|
unique_sdk.Content.delete(
|
|
466
475
|
user_id=state.config.user_id,
|
|
467
476
|
company_id=state.config.company_id,
|
|
@@ -481,6 +490,8 @@ def cmd_mv_file(state: ShellState, old_name: str, new_name: str) -> str:
|
|
|
481
490
|
content_id, display_name = _resolve_content_id(
|
|
482
491
|
state, old_name, allow_chat_files=False
|
|
483
492
|
)
|
|
493
|
+
if state.is_skill_content_write_denied(content_id, new_name=new_name):
|
|
494
|
+
return skill_denial("mv", display_name)
|
|
484
495
|
result = unique_sdk.Content.update(
|
|
485
496
|
user_id=state.config.user_id,
|
|
486
497
|
company_id=state.config.company_id,
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
|
|
7
7
|
import unique_sdk
|
|
8
8
|
from unique_sdk.cli.formatting import format_folder_info
|
|
9
|
+
from unique_sdk.cli.skill_guard import skill_denial
|
|
9
10
|
from unique_sdk.cli.state import ShellState
|
|
10
11
|
|
|
11
12
|
|
|
@@ -59,6 +60,8 @@ def cmd_mkdir(state: ShellState, name: str) -> str:
|
|
|
59
60
|
"mkdir: permission denied: destination is outside your task scope "
|
|
60
61
|
f"({state.scope_denial_hint()})."
|
|
61
62
|
)
|
|
63
|
+
if state.skill_guard.is_path_write_denied(full_path):
|
|
64
|
+
return skill_denial("mkdir", full_path)
|
|
62
65
|
try:
|
|
63
66
|
result = unique_sdk.Folder.create_paths(
|
|
64
67
|
user_id=state.config.user_id,
|
|
@@ -84,11 +87,14 @@ def cmd_rmdir(state: ShellState, target: str, recursive: bool = False) -> str:
|
|
|
84
87
|
and not state.is_folder_target_within_workspace(target)
|
|
85
88
|
):
|
|
86
89
|
return "rmdir: permission denied (outside workspace scope)"
|
|
87
|
-
|
|
90
|
+
scope_id = _resolve_folder_scope_id(state, target)
|
|
91
|
+
if _metadata_filter_denies_folder(state, scope_id):
|
|
88
92
|
return (
|
|
89
93
|
"rmdir: permission denied: target is outside your task scope "
|
|
90
94
|
f"({state.scope_denial_hint()})."
|
|
91
95
|
)
|
|
96
|
+
if state.skill_guard.is_folder_write_denied(scope_id, include_subtree=True):
|
|
97
|
+
return skill_denial("rmdir", target)
|
|
92
98
|
try:
|
|
93
99
|
if target.startswith("scope_"):
|
|
94
100
|
unique_sdk.Folder.delete(
|
|
@@ -122,11 +128,14 @@ def cmd_mvdir(state: ShellState, old_name: str, new_name: str) -> str:
|
|
|
122
128
|
and not state.is_folder_target_within_workspace(old_name)
|
|
123
129
|
):
|
|
124
130
|
return "mvdir: permission denied (outside workspace scope)"
|
|
125
|
-
|
|
131
|
+
scope_id = _resolve_folder_scope_id(state, old_name)
|
|
132
|
+
if _metadata_filter_denies_folder(state, scope_id):
|
|
126
133
|
return (
|
|
127
134
|
"mvdir: permission denied: target is outside your task scope "
|
|
128
135
|
f"({state.scope_denial_hint()})."
|
|
129
136
|
)
|
|
137
|
+
if state.skill_guard.is_folder_write_denied(scope_id, include_subtree=True):
|
|
138
|
+
return skill_denial("mvdir", old_name)
|
|
130
139
|
try:
|
|
131
140
|
if old_name.startswith("scope_"):
|
|
132
141
|
result = unique_sdk.Folder.update(
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/navigation.py
RENAMED
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
import unique_sdk
|
|
8
8
|
from unique_sdk.cli.formatting import format_ls
|
|
9
9
|
from unique_sdk.cli.metadata_filter import _collect_filter_targets
|
|
10
|
+
from unique_sdk.cli.skill_guard import skill_denial
|
|
10
11
|
from unique_sdk.cli.state import ShellState
|
|
11
12
|
|
|
12
13
|
|
|
@@ -43,6 +44,8 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
43
44
|
f"ls: permission denied: target is outside your task scope "
|
|
44
45
|
f"({state.scope_denial_hint()})."
|
|
45
46
|
)
|
|
47
|
+
if scope_id is not None and state.skill_guard.is_folder_hidden(scope_id):
|
|
48
|
+
return skill_denial("ls", target or state.cwd)
|
|
46
49
|
|
|
47
50
|
# At root with a per-message KB scope (e.g. an Agentic Table column's
|
|
48
51
|
# scope_rules), show only the in-scope folders and explicitly-scoped
|
|
@@ -67,6 +70,11 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
67
70
|
)
|
|
68
71
|
except unique_sdk.UniqueError:
|
|
69
72
|
pass
|
|
73
|
+
scoped_folders = [
|
|
74
|
+
folder
|
|
75
|
+
for folder in scoped_folders
|
|
76
|
+
if not state.skill_guard.is_folder_hidden(folder.get("id", ""))
|
|
77
|
+
]
|
|
70
78
|
scoped_files: list[Any] = []
|
|
71
79
|
for cid in content_ids:
|
|
72
80
|
# A contentId mentioned in the filter is not necessarily in
|
|
@@ -77,6 +85,8 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
77
85
|
# UN-21780.
|
|
78
86
|
if not state.is_content_within_workspace(cid):
|
|
79
87
|
continue
|
|
88
|
+
if state.is_skill_content_read_denied(cid):
|
|
89
|
+
continue
|
|
80
90
|
try:
|
|
81
91
|
info = unique_sdk.Content.get_info(
|
|
82
92
|
user_id=state.config.user_id,
|
|
@@ -109,6 +119,11 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
109
119
|
folders.append(info)
|
|
110
120
|
except unique_sdk.UniqueError:
|
|
111
121
|
pass
|
|
122
|
+
folders = [
|
|
123
|
+
folder
|
|
124
|
+
for folder in folders
|
|
125
|
+
if not state.skill_guard.is_folder_hidden(folder.get("id", ""))
|
|
126
|
+
]
|
|
112
127
|
output = format_ls(folders, [])
|
|
113
128
|
summary = f"\n{len(folders)} folder(s), 0 file(s)"
|
|
114
129
|
return output + summary
|
|
@@ -124,7 +139,8 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
124
139
|
company_id=state.config.company_id,
|
|
125
140
|
**folder_params,
|
|
126
141
|
)
|
|
127
|
-
|
|
142
|
+
listed_folders = folder_result.get("folderInfos", [])
|
|
143
|
+
folders = state.skill_guard.visible_child_folders(listed_folders)
|
|
128
144
|
|
|
129
145
|
content_result = unique_sdk.Content.get_infos(
|
|
130
146
|
user_id=state.config.user_id,
|
|
@@ -133,7 +149,9 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
133
149
|
)
|
|
134
150
|
files = content_result.get("contentInfos", [])
|
|
135
151
|
|
|
136
|
-
total_folders = folder_result.get("totalCount", len(
|
|
152
|
+
total_folders = folder_result.get("totalCount", len(listed_folders)) - (
|
|
153
|
+
len(listed_folders) - len(folders)
|
|
154
|
+
)
|
|
137
155
|
total_files = content_result.get("totalCount", len(files))
|
|
138
156
|
|
|
139
157
|
# With a per-message filter, listing inside an allowed folder must not
|
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
20
|
import unique_sdk
|
|
21
|
+
from unique_sdk.cli.skill_guard import skill_denial
|
|
21
22
|
from unique_sdk.cli.state import ShellState
|
|
22
23
|
|
|
23
24
|
READ_ERROR_PREFIX = "read:"
|
|
@@ -103,6 +104,8 @@ def cmd_read(
|
|
|
103
104
|
f"task scope ({state.scope_denial_hint()}). Use 'unique-cli search' "
|
|
104
105
|
"or 'ls' within that scope instead."
|
|
105
106
|
)
|
|
107
|
+
if state.is_skill_content_read_denied(cont_id):
|
|
108
|
+
return skill_denial("read", cont_id)
|
|
106
109
|
|
|
107
110
|
try:
|
|
108
111
|
results = unique_sdk.Content.search(
|
|
@@ -280,6 +280,7 @@ def cmd_search(
|
|
|
280
280
|
company_id=state.config.company_id,
|
|
281
281
|
**search_params,
|
|
282
282
|
)
|
|
283
|
+
results = [hit for hit in results if not state.is_skill_search_hit_hidden(hit)]
|
|
283
284
|
|
|
284
285
|
except (ValueError, unique_sdk.UniqueError) as e:
|
|
285
286
|
return f"{SEARCH_ERROR_PREFIX} {e}"
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Keep CLI reads and writes out of skill folders (a ``skill.md``/``skills.md`` marker
|
|
2
|
+
at or above the folder) outside the caller's personal skills
|
|
3
|
+
(``<home>/skills-conduct/space-<spaceId>``)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import unique_sdk
|
|
14
|
+
from unique_sdk.cli.config import Config
|
|
15
|
+
|
|
16
|
+
_MARKER_NAME_RE = re.compile(r"^skills?\.md$", re.IGNORECASE)
|
|
17
|
+
|
|
18
|
+
# The SDK JSON-encodes request params without an Enum encoder, so
|
|
19
|
+
# ``mode: insensitive`` can't be sent. Match the common spellings instead and
|
|
20
|
+
# re-check every hit with ``is_skill_marker_name``.
|
|
21
|
+
_MARKER_KEY_SUFFIXES = tuple(
|
|
22
|
+
f"{stem}{extension}"
|
|
23
|
+
for stem in ("skill", "Skill", "SKILL", "skills", "Skills", "SKILLS")
|
|
24
|
+
for extension in (".md", ".MD")
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_FOLDER_ID_PATH_PREFIX = "uniquepathid://"
|
|
28
|
+
_MAX_FOLDER_DEPTH = 64
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PersonalSkillsFolder(StrEnum):
|
|
32
|
+
"""Folder names in ``/home/<UserName>/skills-conduct/space-<spaceId>``."""
|
|
33
|
+
|
|
34
|
+
HOME_ROOT = "home"
|
|
35
|
+
LEGACY_HOME_PREFIX = "home-"
|
|
36
|
+
SKILLS = "skills-conduct"
|
|
37
|
+
SPACE_PREFIX = "space-"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_skill_marker_name(name: str) -> bool:
|
|
41
|
+
"""True when the last segment of *name* is a skill marker file name."""
|
|
42
|
+
base = name.replace("\\", "/").rsplit("/", 1)[-1].strip()
|
|
43
|
+
return bool(_MARKER_NAME_RE.match(base))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def skill_denial(command: str, target: str) -> str:
|
|
47
|
+
"""Denial text in the ``<command>: permission denied`` shape the CLI exits on."""
|
|
48
|
+
return (
|
|
49
|
+
f"{command}: permission denied: {target} is part of a skill outside your "
|
|
50
|
+
"personal skills, or its skill status could not be checked."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class _FolderNode:
|
|
56
|
+
scope_id: str
|
|
57
|
+
name: str
|
|
58
|
+
parent_id: str | None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class _SkillMarker:
|
|
63
|
+
skill_folder_id: str
|
|
64
|
+
# Empty when the content has no ``folderIdPath``; looked up on demand.
|
|
65
|
+
folder_ids: frozenset[str]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SkillGuard:
|
|
69
|
+
"""Answers whether a read or write would touch a protected skill."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, config: Config) -> None:
|
|
72
|
+
self._config = config
|
|
73
|
+
self._folders: dict[str, _FolderNode] = {}
|
|
74
|
+
self._markers: tuple[_SkillMarker, ...] | None = None
|
|
75
|
+
self._hidden: dict[str, bool] = {}
|
|
76
|
+
|
|
77
|
+
def is_folder_hidden(self, scope_id: str) -> bool:
|
|
78
|
+
"""True when *scope_id* is inside a protected skill. Fails closed."""
|
|
79
|
+
cached = self._hidden.get(scope_id)
|
|
80
|
+
if cached is not None:
|
|
81
|
+
return cached
|
|
82
|
+
try:
|
|
83
|
+
chain = self._folder_chain(scope_id)
|
|
84
|
+
hidden = not self._is_in_own_personal_layer(chain) and self._has_marker_in(
|
|
85
|
+
[node.scope_id for node in chain]
|
|
86
|
+
)
|
|
87
|
+
except unique_sdk.UniqueError:
|
|
88
|
+
hidden = True
|
|
89
|
+
self._hidden[scope_id] = hidden
|
|
90
|
+
return hidden
|
|
91
|
+
|
|
92
|
+
def is_folder_path_hidden(self, folder_ids: Sequence[str]) -> bool:
|
|
93
|
+
"""True when a ``folderIdPath`` runs through a protected skill. Fails closed."""
|
|
94
|
+
try:
|
|
95
|
+
skill_folder_ids = {
|
|
96
|
+
marker.skill_folder_id for marker in self._all_skill_markers()
|
|
97
|
+
}
|
|
98
|
+
except unique_sdk.UniqueError:
|
|
99
|
+
return True
|
|
100
|
+
return any(
|
|
101
|
+
self.is_folder_hidden(folder_id)
|
|
102
|
+
for folder_id in folder_ids
|
|
103
|
+
if folder_id in skill_folder_ids
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def visible_child_folders(self, folders: Sequence[Any]) -> list[Any]:
|
|
107
|
+
"""Drop protected skills from the child folders of a visible folder."""
|
|
108
|
+
folder_ids = [folder.get("id") for folder in folders if folder.get("id")]
|
|
109
|
+
if not folder_ids:
|
|
110
|
+
return list(folders)
|
|
111
|
+
skill_folder_ids = {
|
|
112
|
+
_marker_owner_id(content)
|
|
113
|
+
for content in self._search_markers({"ownerId": {"in_": folder_ids}})
|
|
114
|
+
if is_skill_marker_name(content.get("key") or "")
|
|
115
|
+
}
|
|
116
|
+
return [
|
|
117
|
+
folder
|
|
118
|
+
for folder in folders
|
|
119
|
+
if folder.get("id") not in skill_folder_ids
|
|
120
|
+
or not self.is_folder_hidden(folder["id"])
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
def is_folder_write_denied(
|
|
124
|
+
self,
|
|
125
|
+
scope_id: str | None,
|
|
126
|
+
*,
|
|
127
|
+
include_subtree: bool = False,
|
|
128
|
+
new_file_name: str | None = None,
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""True when writing in *scope_id* would change a protected skill.
|
|
131
|
+
|
|
132
|
+
``include_subtree`` also protects skills below the folder (``rmdir``,
|
|
133
|
+
``mvdir``). ``new_file_name`` protects against creating a marker, which
|
|
134
|
+
would turn the folder into a skill. Fails closed when the folder is
|
|
135
|
+
unknown or a lookup fails.
|
|
136
|
+
"""
|
|
137
|
+
if not scope_id:
|
|
138
|
+
return True
|
|
139
|
+
try:
|
|
140
|
+
return self._is_folder_write_denied(
|
|
141
|
+
scope_id,
|
|
142
|
+
include_subtree=include_subtree,
|
|
143
|
+
new_file_name=new_file_name,
|
|
144
|
+
)
|
|
145
|
+
except unique_sdk.UniqueError:
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
def is_path_write_denied(self, folder_path: str) -> bool:
|
|
149
|
+
"""True when creating *folder_path* would add structure inside a protected skill.
|
|
150
|
+
|
|
151
|
+
Checks the deepest existing folder on the path, because the target does
|
|
152
|
+
not exist yet.
|
|
153
|
+
"""
|
|
154
|
+
segments = [segment for segment in folder_path.split("/") if segment]
|
|
155
|
+
while segments:
|
|
156
|
+
candidate = "/" + "/".join(segments)
|
|
157
|
+
try:
|
|
158
|
+
info = unique_sdk.Folder.get_info(
|
|
159
|
+
user_id=self._config.user_id,
|
|
160
|
+
company_id=self._config.company_id,
|
|
161
|
+
folderPath=candidate,
|
|
162
|
+
)
|
|
163
|
+
except unique_sdk.UniqueError:
|
|
164
|
+
segments.pop()
|
|
165
|
+
continue
|
|
166
|
+
scope_id = info.get("id")
|
|
167
|
+
if not scope_id:
|
|
168
|
+
return True
|
|
169
|
+
return self.is_folder_write_denied(scope_id)
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
def _is_folder_write_denied(
|
|
173
|
+
self,
|
|
174
|
+
scope_id: str,
|
|
175
|
+
*,
|
|
176
|
+
include_subtree: bool,
|
|
177
|
+
new_file_name: str | None,
|
|
178
|
+
) -> bool:
|
|
179
|
+
chain = self._folder_chain(scope_id)
|
|
180
|
+
if self._is_in_own_personal_layer(chain):
|
|
181
|
+
return False
|
|
182
|
+
if new_file_name is not None and is_skill_marker_name(new_file_name):
|
|
183
|
+
return True
|
|
184
|
+
if self._has_marker_in([node.scope_id for node in chain]):
|
|
185
|
+
return True
|
|
186
|
+
if include_subtree:
|
|
187
|
+
return any(
|
|
188
|
+
scope_id in self._marker_folder_ids(marker)
|
|
189
|
+
for marker in self._all_skill_markers()
|
|
190
|
+
)
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def _is_in_own_personal_layer(self, chain: list[_FolderNode]) -> bool:
|
|
194
|
+
"""True inside ``<home>/skills-conduct/space-<spaceId>``.
|
|
195
|
+
|
|
196
|
+
The API resolves no other user's home for the caller, so a home in the
|
|
197
|
+
chain is the caller's own. The API re-checks the owner on write.
|
|
198
|
+
"""
|
|
199
|
+
return any(
|
|
200
|
+
node.name.startswith(PersonalSkillsFolder.SPACE_PREFIX)
|
|
201
|
+
and len(chain) > index + 2
|
|
202
|
+
and chain[index + 1].name == PersonalSkillsFolder.SKILLS
|
|
203
|
+
and self._is_own_home(chain[index + 2 :])
|
|
204
|
+
for index, node in enumerate(chain)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _is_own_home(self, chain_from_home: list[_FolderNode]) -> bool:
|
|
208
|
+
"""``/home/<UserName>``, or the legacy root ``/home-<userId>``."""
|
|
209
|
+
home, *above = chain_from_home
|
|
210
|
+
if not above:
|
|
211
|
+
return (
|
|
212
|
+
home.name
|
|
213
|
+
== f"{PersonalSkillsFolder.LEGACY_HOME_PREFIX}{self._config.user_id}"
|
|
214
|
+
)
|
|
215
|
+
return (
|
|
216
|
+
len(above) == 1
|
|
217
|
+
and above[0].name == PersonalSkillsFolder.HOME_ROOT
|
|
218
|
+
and above[0].parent_id is None
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def _has_marker_in(self, folder_ids: list[str]) -> bool:
|
|
222
|
+
if not folder_ids:
|
|
223
|
+
return False
|
|
224
|
+
results = self._search_markers({"ownerId": {"in_": folder_ids}})
|
|
225
|
+
return any(
|
|
226
|
+
is_skill_marker_name(content.get("key") or "") for content in results
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _marker_folder_ids(self, marker: _SkillMarker) -> frozenset[str]:
|
|
230
|
+
if marker.folder_ids:
|
|
231
|
+
return marker.folder_ids
|
|
232
|
+
return frozenset(
|
|
233
|
+
node.scope_id for node in self._folder_chain(marker.skill_folder_id)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def _folder_chain(self, scope_id: str) -> list[_FolderNode]:
|
|
237
|
+
"""The folder and its ancestors, nearest first."""
|
|
238
|
+
chain: list[_FolderNode] = []
|
|
239
|
+
current: str | None = scope_id
|
|
240
|
+
while current and len(chain) < _MAX_FOLDER_DEPTH:
|
|
241
|
+
node = self._folder(current)
|
|
242
|
+
chain.append(node)
|
|
243
|
+
current = node.parent_id
|
|
244
|
+
return chain
|
|
245
|
+
|
|
246
|
+
def _folder(self, scope_id: str) -> _FolderNode:
|
|
247
|
+
cached = self._folders.get(scope_id)
|
|
248
|
+
if cached is not None:
|
|
249
|
+
return cached
|
|
250
|
+
info = unique_sdk.Folder.get_info(
|
|
251
|
+
user_id=self._config.user_id,
|
|
252
|
+
company_id=self._config.company_id,
|
|
253
|
+
scopeId=scope_id,
|
|
254
|
+
)
|
|
255
|
+
node = _FolderNode(
|
|
256
|
+
scope_id=info["id"],
|
|
257
|
+
name=info.get("name") or "",
|
|
258
|
+
parent_id=info.get("parentId"),
|
|
259
|
+
)
|
|
260
|
+
self._folders[scope_id] = node
|
|
261
|
+
return node
|
|
262
|
+
|
|
263
|
+
def _search_markers(
|
|
264
|
+
self, where: unique_sdk.Content.ContentWhereInput | None = None
|
|
265
|
+
) -> list[unique_sdk.Content]:
|
|
266
|
+
key_filter: unique_sdk.Content.ContentWhereInput = {
|
|
267
|
+
"OR": [{"key": {"endsWith": suffix}} for suffix in _MARKER_KEY_SUFFIXES]
|
|
268
|
+
}
|
|
269
|
+
clauses = [key_filter] if where is None else [key_filter, where]
|
|
270
|
+
return unique_sdk.Content.search(
|
|
271
|
+
user_id=self._config.user_id,
|
|
272
|
+
company_id=self._config.company_id,
|
|
273
|
+
where={"AND": clauses},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def _all_skill_markers(self) -> tuple[_SkillMarker, ...]:
|
|
277
|
+
"""Every marker the caller can read. Only subtree checks need this."""
|
|
278
|
+
if self._markers is not None:
|
|
279
|
+
return self._markers
|
|
280
|
+
markers: list[_SkillMarker] = []
|
|
281
|
+
for content in self._search_markers():
|
|
282
|
+
if not is_skill_marker_name(content.get("key") or ""):
|
|
283
|
+
continue
|
|
284
|
+
path_ids = folder_ids_from_metadata(content.get("metadata"))
|
|
285
|
+
owner_id = _marker_owner_id(content)
|
|
286
|
+
if not owner_id.startswith("scope_"):
|
|
287
|
+
continue
|
|
288
|
+
markers.append(
|
|
289
|
+
_SkillMarker(
|
|
290
|
+
skill_folder_id=owner_id,
|
|
291
|
+
folder_ids=frozenset({owner_id, *path_ids})
|
|
292
|
+
if path_ids
|
|
293
|
+
else frozenset(),
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
self._markers = tuple(markers)
|
|
297
|
+
return self._markers
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _marker_owner_id(content: unique_sdk.Content) -> str:
|
|
301
|
+
"""The marker's folder: ``ownerId``, else the last ``folderIdPath`` segment."""
|
|
302
|
+
path_ids = folder_ids_from_metadata(content.get("metadata"))
|
|
303
|
+
return content.get("ownerId") or (path_ids[-1] if path_ids else "")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def folder_ids_from_metadata(metadata: object) -> list[str]:
|
|
307
|
+
"""Folder ids in a content's ``folderIdPath``, or ``[]`` when absent."""
|
|
308
|
+
if not isinstance(metadata, dict):
|
|
309
|
+
return []
|
|
310
|
+
folder_id_path = metadata.get("folderIdPath")
|
|
311
|
+
if not isinstance(folder_id_path, str):
|
|
312
|
+
return []
|
|
313
|
+
path = folder_id_path.removeprefix(_FOLDER_ID_PATH_PREFIX)
|
|
314
|
+
return [segment for segment in path.split("/") if segment.startswith("scope_")]
|
|
@@ -288,6 +288,7 @@ verified page numbers (if any) and `--read-method`.
|
|
|
288
288
|
- File-not-found and folder-not-found errors are returned as text, not exceptions.
|
|
289
289
|
- Successful results print to stdout -- parse output as needed.
|
|
290
290
|
- Scope denials (e.g. a file or folder outside the task scope) print to **stderr** and exit with a **non-zero** status, so a denial in an `&&` chain stops the chain instead of being treated as success. Read the stderr message: it names the in-scope folders/documents to redirect you, rather than retrying the same out-of-scope target.
|
|
291
|
+
- Skills in the knowledge base are off limits. A skill is a folder with a `SKILL.md` (or `skills.md`) file, including everything inside it. `search` and `ls` hide them, and `read`, `download`, `upload`, `rm`, `mv`, `mkdir`, `rmdir` and `mvdir` refuse them. Only your personal skills folders (`<home>/skills-conduct/space-<spaceId>` in your own home) are exempt. Never retry a denied command another way.
|
|
291
292
|
|
|
292
293
|
## Interactive Mode
|
|
293
294
|
|
|
@@ -10,6 +10,7 @@ from typing import Any
|
|
|
10
10
|
import unique_sdk
|
|
11
11
|
from unique_sdk.cli.config import Config
|
|
12
12
|
from unique_sdk.cli.metadata_filter import MetadataFilter
|
|
13
|
+
from unique_sdk.cli.skill_guard import SkillGuard, folder_ids_from_metadata
|
|
13
14
|
from unique_sdk.cli.workspace import manifest_path as workspace_manifest_path
|
|
14
15
|
|
|
15
16
|
_SEARCH_CONFIG_FILENAME = ".unique-search.json"
|
|
@@ -180,6 +181,9 @@ class ShellState:
|
|
|
180
181
|
self.mcp_tool_reference_mappings: dict[str, dict[str, Any]] = (
|
|
181
182
|
_load_mcp_tool_configs()
|
|
182
183
|
)
|
|
184
|
+
# Independent of scopeIds and metaDataFilter: a skill folder inside the
|
|
185
|
+
# KB scope is still off limits for writes. See UN-25784.
|
|
186
|
+
self.skill_guard = SkillGuard(config)
|
|
183
187
|
|
|
184
188
|
@property
|
|
185
189
|
def uploaded_search_available(self) -> bool:
|
|
@@ -420,6 +424,44 @@ class ShellState:
|
|
|
420
424
|
self._content_info_cache[content_id] = info
|
|
421
425
|
return info
|
|
422
426
|
|
|
427
|
+
def is_skill_content_write_denied(
|
|
428
|
+
self, content_id: str, *, new_name: str | None = None
|
|
429
|
+
) -> bool:
|
|
430
|
+
"""True when changing *content_id* would change a protected skill.
|
|
431
|
+
|
|
432
|
+
Content owned by a chat instead of a folder is never part of a skill.
|
|
433
|
+
Fails closed when the content can't be looked up.
|
|
434
|
+
"""
|
|
435
|
+
info = self._get_content_info(content_id)
|
|
436
|
+
if not info:
|
|
437
|
+
return True
|
|
438
|
+
owner_id = info.get("ownerId") or ""
|
|
439
|
+
if not owner_id.startswith("scope_"):
|
|
440
|
+
return False
|
|
441
|
+
return self.skill_guard.is_folder_write_denied(owner_id, new_file_name=new_name)
|
|
442
|
+
|
|
443
|
+
def is_skill_content_read_denied(self, content_id: str) -> bool:
|
|
444
|
+
"""True when *content_id* is part of a protected skill.
|
|
445
|
+
|
|
446
|
+
Content owned by a chat instead of a folder is never part of a skill.
|
|
447
|
+
Fails closed when the content can't be looked up.
|
|
448
|
+
"""
|
|
449
|
+
info = self._get_content_info(content_id)
|
|
450
|
+
if not info:
|
|
451
|
+
return True
|
|
452
|
+
owner_id = info.get("ownerId") or ""
|
|
453
|
+
if not owner_id.startswith("scope_"):
|
|
454
|
+
return False
|
|
455
|
+
return self.skill_guard.is_folder_hidden(owner_id)
|
|
456
|
+
|
|
457
|
+
def is_skill_search_hit_hidden(self, hit: Any) -> bool:
|
|
458
|
+
"""True when a search result sits in a protected skill."""
|
|
459
|
+
folder_ids = folder_ids_from_metadata(getattr(hit, "metadata", None))
|
|
460
|
+
if folder_ids:
|
|
461
|
+
return self.skill_guard.is_folder_path_hidden(folder_ids)
|
|
462
|
+
content_id = getattr(hit, "id", "") or ""
|
|
463
|
+
return bool(content_id) and self.is_skill_content_read_denied(content_id)
|
|
464
|
+
|
|
423
465
|
def resolve_content_title(self, content_id: str) -> str:
|
|
424
466
|
"""Human-readable title for a content id, falling back to the id.
|
|
425
467
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/__init__.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_acronyms.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_agentic_table.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_analytics_order.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_benchmarking.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_briefing.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_chat_completion.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_context_memory.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_elicitation.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_embedding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_integrated.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_llm_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_message_log.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_message_tool.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_scheduled_task.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_search_string.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/api_resources/_web_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/agentic_table.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/cite_file.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/elicitation.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/scheduled_tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/web_search.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/cli/commands/web_search_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/utils/analytics_order_run.py
RENAMED
|
File without changes
|
{unique_sdk-2026.42.0.dev3 → unique_sdk-2026.42.0.dev4}/unique_sdk/utils/benchmarking_run.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|