pal-found-cli 0.1.2__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.
- pal_found_cli/__init__.py +1 -0
- pal_found_cli/_version.py +24 -0
- pal_found_cli/admin/__init__.py +2 -0
- pal_found_cli/admin/scripts/__init__.py +2 -0
- pal_found_cli/admin/scripts/pal_found_admin_cli.py +600 -0
- pal_found_cli/aip_agents/__init__.py +1 -0
- pal_found_cli/aip_agents/metadata-allow-list.md +20 -0
- pal_found_cli/aip_agents/scripts/__init__.py +1 -0
- pal_found_cli/aip_agents/scripts/pal_found_aip_agents_cli.py +704 -0
- pal_found_cli/audit/__init__.py +1 -0
- pal_found_cli/audit/metadata-allow-list.md +4 -0
- pal_found_cli/audit/scripts/__init__.py +1 -0
- pal_found_cli/audit/scripts/pal_found_audit_cli.py +387 -0
- pal_found_cli/checkpoints/__init__.py +9 -0
- pal_found_cli/checkpoints/metadata-allow-list.md +7 -0
- pal_found_cli/checkpoints/scripts/__init__.py +1 -0
- pal_found_cli/checkpoints/scripts/pal_found_checkpoints_cli.py +527 -0
- pal_found_cli/common/__init__.py +56 -0
- pal_found_cli/common/access_control_guard.py +591 -0
- pal_found_cli/common/async_client_factory.py +161 -0
- pal_found_cli/common/auth_provider.py +82 -0
- pal_found_cli/common/binary_download_handler.py +288 -0
- pal_found_cli/common/config_loader.py +388 -0
- pal_found_cli/common/error_serializer.py +412 -0
- pal_found_cli/common/log_setup.py +241 -0
- pal_found_cli/common/metadata-allow-list.md +169 -0
- pal_found_cli/common/output_formatter.py +267 -0
- pal_found_cli/common/pagination_helper.py +212 -0
- pal_found_cli/common/retry.py +617 -0
- pal_found_cli/common/sdk_error_utils.py +112 -0
- pal_found_cli/common/session_manager.py +461 -0
- pal_found_cli/common/tracing_provider.py +107 -0
- pal_found_cli/connectivity/__init__.py +9 -0
- pal_found_cli/connectivity/metadata-allow-list.md +24 -0
- pal_found_cli/connectivity/scripts/__init__.py +1 -0
- pal_found_cli/connectivity/scripts/pal_found_connectivity_cli.py +720 -0
- pal_found_cli/data_health/__init__.py +9 -0
- pal_found_cli/data_health/metadata-allow-list.md +10 -0
- pal_found_cli/data_health/scripts/__init__.py +1 -0
- pal_found_cli/data_health/scripts/pal_found_data_health_cli.py +458 -0
- pal_found_cli/datasets/__init__.py +1 -0
- pal_found_cli/datasets/scripts/__init__.py +1 -0
- pal_found_cli/datasets/scripts/pal_found_datasets_cli.py +732 -0
- pal_found_cli/filesystem/__init__.py +2 -0
- pal_found_cli/filesystem/scripts/__init__.py +2 -0
- pal_found_cli/filesystem/scripts/pal_found_filesystem_cli.py +659 -0
- pal_found_cli/functions/__init__.py +2 -0
- pal_found_cli/functions/scripts/__init__.py +2 -0
- pal_found_cli/functions/scripts/pal_found_functions_cli.py +389 -0
- pal_found_cli/language_models/__init__.py +1 -0
- pal_found_cli/language_models/metadata-allow-list.md +6 -0
- pal_found_cli/language_models/scripts/__init__.py +1 -0
- pal_found_cli/language_models/scripts/pal_found_language_models_cli.py +342 -0
- pal_found_cli/media_sets/__init__.py +9 -0
- pal_found_cli/media_sets/metadata-allow-list.md +23 -0
- pal_found_cli/media_sets/scripts/__init__.py +1 -0
- pal_found_cli/media_sets/scripts/pal_found_media_sets_cli.py +642 -0
- pal_found_cli/models/__init__.py +5 -0
- pal_found_cli/models/metadata-allow-list.md +27 -0
- pal_found_cli/models/scripts/__init__.py +1 -0
- pal_found_cli/models/scripts/pal_found_models_cli.py +714 -0
- pal_found_cli/ontologies/__init__.py +1 -0
- pal_found_cli/ontologies/scripts/__init__.py +1 -0
- pal_found_cli/ontologies/scripts/pal_found_ontologies_cli.py +1225 -0
- pal_found_cli/orchestration/__init__.py +9 -0
- pal_found_cli/orchestration/metadata-allow-list.md +24 -0
- pal_found_cli/orchestration/scripts/__init__.py +1 -0
- pal_found_cli/orchestration/scripts/pal_found_orchestration_cli.py +640 -0
- pal_found_cli/sql_queries/__init__.py +9 -0
- pal_found_cli/sql_queries/metadata-allow-list.md +9 -0
- pal_found_cli/sql_queries/scripts/__init__.py +1 -0
- pal_found_cli/sql_queries/scripts/pal_found_sql_queries_cli.py +514 -0
- pal_found_cli/streams/__init__.py +9 -0
- pal_found_cli/streams/metadata-allow-list.md +19 -0
- pal_found_cli/streams/scripts/__init__.py +1 -0
- pal_found_cli/streams/scripts/pal_found_streams_cli.py +615 -0
- pal_found_cli/third_party_applications/__init__.py +9 -0
- pal_found_cli/third_party_applications/metadata-allow-list.md +13 -0
- pal_found_cli/third_party_applications/scripts/__init__.py +1 -0
- pal_found_cli/third_party_applications/scripts/pal_found_third_party_applications_cli.py +547 -0
- pal_found_cli/widgets/__init__.py +9 -0
- pal_found_cli/widgets/metadata-allow-list.md +12 -0
- pal_found_cli/widgets/scripts/__init__.py +1 -0
- pal_found_cli/widgets/scripts/pal_found_widgets_cli.py +571 -0
- pal_found_cli-0.1.2.dist-info/METADATA +74 -0
- pal_found_cli-0.1.2.dist-info/RECORD +89 -0
- pal_found_cli-0.1.2.dist-info/WHEEL +5 -0
- pal_found_cli-0.1.2.dist-info/entry_points.txt +19 -0
- pal_found_cli-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Foundry AIP Agents CLI with local alias lifecycle management."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import AsyncIterator, Iterable
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, NoReturn
|
|
15
|
+
|
|
16
|
+
from pal_found_cli.common.access_control_guard import AccessControlGuard
|
|
17
|
+
from pal_found_cli.common.async_client_factory import AsyncClientFactory
|
|
18
|
+
from pal_found_cli.common.binary_download_handler import BinaryDownloadHandler
|
|
19
|
+
from pal_found_cli.common.config_loader import ConfigLoader, ConfigurationError
|
|
20
|
+
from pal_found_cli.common.error_serializer import (
|
|
21
|
+
EXIT_ACCESS_CONTROL,
|
|
22
|
+
EXIT_AUTH,
|
|
23
|
+
EXIT_CONFIGURATION,
|
|
24
|
+
EXIT_NOT_FOUND,
|
|
25
|
+
EXIT_PERMISSION_DENIED,
|
|
26
|
+
EXIT_RATE_LIMIT,
|
|
27
|
+
EXIT_SERVER_ERROR,
|
|
28
|
+
EXIT_SUCCESS,
|
|
29
|
+
EXIT_TIMEOUT,
|
|
30
|
+
EXIT_USER_INPUT,
|
|
31
|
+
ErrorSerializer,
|
|
32
|
+
)
|
|
33
|
+
from pal_found_cli.common.log_setup import LogSetup
|
|
34
|
+
from pal_found_cli.common.output_formatter import OutputFormatter
|
|
35
|
+
from pal_found_cli.common.pagination_helper import PaginationHelper
|
|
36
|
+
from pal_found_cli.common.retry import RetryHandler
|
|
37
|
+
from pal_found_cli.common.sdk_error_utils import sdk_exception_exit_code, sdk_http_status
|
|
38
|
+
from pal_found_cli.common.session_manager import SessionManager, SessionState
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
OperationSpec = dict[str, Any]
|
|
43
|
+
_METADATA_ALLOWLIST_PATH = Path(__file__).resolve().parents[1] / "metadata-allow-list.md"
|
|
44
|
+
_PAGED = frozenset(
|
|
45
|
+
{("agent", "all_sessions"), ("agent_version", "list"), ("session", "list")}
|
|
46
|
+
)
|
|
47
|
+
_ALIAS_BOUND = frozenset(
|
|
48
|
+
{
|
|
49
|
+
("session", "blocking_continue"),
|
|
50
|
+
("session", "cancel"),
|
|
51
|
+
("session", "delete"),
|
|
52
|
+
("session", "get"),
|
|
53
|
+
("session", "rag_context"),
|
|
54
|
+
("session", "streaming_continue"),
|
|
55
|
+
("session", "update_title"),
|
|
56
|
+
("content", "get"),
|
|
57
|
+
("session_trace", "get"),
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _ArgumentParser(argparse.ArgumentParser):
|
|
63
|
+
"""Raise parser failures into structured error handling."""
|
|
64
|
+
|
|
65
|
+
def error(self, message: str) -> NoReturn:
|
|
66
|
+
raise ValueError(message)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SDKContractError(Exception):
|
|
70
|
+
"""Raised when an SDK result violates its documented response contract."""
|
|
71
|
+
|
|
72
|
+
exit_code = EXIT_SERVER_ERROR
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _op(
|
|
76
|
+
resource: str,
|
|
77
|
+
operation: str,
|
|
78
|
+
client_path: str,
|
|
79
|
+
method: str,
|
|
80
|
+
positional: Iterable[str] = (),
|
|
81
|
+
required: Iterable[str] = (),
|
|
82
|
+
optional: Iterable[str] = (),
|
|
83
|
+
*,
|
|
84
|
+
json_objects: Iterable[str] = (),
|
|
85
|
+
json_lists: Iterable[str] = (),
|
|
86
|
+
) -> OperationSpec:
|
|
87
|
+
"""Build one SDK operation specification."""
|
|
88
|
+
return {
|
|
89
|
+
"resource": resource,
|
|
90
|
+
"operation": operation,
|
|
91
|
+
"client_path": tuple(client_path.split(".")),
|
|
92
|
+
"method": method,
|
|
93
|
+
"positional": tuple(positional),
|
|
94
|
+
"required": tuple(required),
|
|
95
|
+
"optional": tuple(optional),
|
|
96
|
+
"json_objects": frozenset(json_objects),
|
|
97
|
+
"json_lists": frozenset(json_lists),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
OP_SPECS: tuple[OperationSpec, ...] = (
|
|
102
|
+
_op("agent", "all_sessions", "Agent", "all_sessions"),
|
|
103
|
+
_op("agent", "get", "Agent", "get", ("agent_rid",), optional=("version",)),
|
|
104
|
+
_op(
|
|
105
|
+
"agent_version",
|
|
106
|
+
"get",
|
|
107
|
+
"Agent.AgentVersion",
|
|
108
|
+
"get",
|
|
109
|
+
("agent_rid", "agent_version_string"),
|
|
110
|
+
),
|
|
111
|
+
_op("agent_version", "list", "Agent.AgentVersion", "list", ("agent_rid",)),
|
|
112
|
+
_op(
|
|
113
|
+
"session",
|
|
114
|
+
"blocking_continue",
|
|
115
|
+
"Agent.Session",
|
|
116
|
+
"blocking_continue",
|
|
117
|
+
required=("parameter_inputs", "user_input"),
|
|
118
|
+
optional=("contexts_override", "session_trace_id"),
|
|
119
|
+
json_objects=("parameter_inputs", "user_input"),
|
|
120
|
+
json_lists=("contexts_override",),
|
|
121
|
+
),
|
|
122
|
+
_op(
|
|
123
|
+
"session",
|
|
124
|
+
"cancel",
|
|
125
|
+
"Agent.Session",
|
|
126
|
+
"cancel",
|
|
127
|
+
required=("message_id",),
|
|
128
|
+
optional=("response",),
|
|
129
|
+
),
|
|
130
|
+
_op("session", "create", "Agent.Session", "create", required=("alias", "agent_rid"), optional=("agent_version",)),
|
|
131
|
+
_op("session", "delete", "Agent.Session", "delete"),
|
|
132
|
+
_op("session", "get", "Agent.Session", "get"),
|
|
133
|
+
_op("session", "list", "Agent.Session", "list", ("agent_rid",)),
|
|
134
|
+
_op(
|
|
135
|
+
"session",
|
|
136
|
+
"rag_context",
|
|
137
|
+
"Agent.Session",
|
|
138
|
+
"rag_context",
|
|
139
|
+
required=("parameter_inputs", "user_input"),
|
|
140
|
+
json_objects=("parameter_inputs", "user_input"),
|
|
141
|
+
),
|
|
142
|
+
_op(
|
|
143
|
+
"session",
|
|
144
|
+
"streaming_continue",
|
|
145
|
+
"Agent.Session",
|
|
146
|
+
"streaming_continue",
|
|
147
|
+
required=("parameter_inputs", "user_input"),
|
|
148
|
+
optional=("contexts_override", "message_id", "session_trace_id", "output_filename"),
|
|
149
|
+
json_objects=("parameter_inputs", "user_input"),
|
|
150
|
+
json_lists=("contexts_override",),
|
|
151
|
+
),
|
|
152
|
+
_op("session", "update_title", "Agent.Session", "update_title", required=("title",)),
|
|
153
|
+
_op("content", "get", "Agent.Session.Content", "get"),
|
|
154
|
+
_op(
|
|
155
|
+
"session_trace",
|
|
156
|
+
"get",
|
|
157
|
+
"Agent.Session.SessionTrace",
|
|
158
|
+
"get",
|
|
159
|
+
required=("session_trace_id",),
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
OPERATION_BY_RESOURCE: dict[str, dict[str, OperationSpec]] = {}
|
|
164
|
+
for _operation_spec in OP_SPECS:
|
|
165
|
+
OPERATION_BY_RESOURCE.setdefault(_operation_spec["resource"], {})[
|
|
166
|
+
_operation_spec["operation"]
|
|
167
|
+
] = _operation_spec
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _common_parser(*, paged: bool, timeout: bool = True) -> _ArgumentParser:
|
|
171
|
+
"""Create operation-level common options."""
|
|
172
|
+
parser = _ArgumentParser(add_help=False)
|
|
173
|
+
if timeout:
|
|
174
|
+
parser.add_argument("--timeout", type=int, default=None)
|
|
175
|
+
parser.add_argument("--format", choices=("json", "toon", "auto"), default="auto")
|
|
176
|
+
parser.add_argument("--pretty", action="store_true")
|
|
177
|
+
if paged:
|
|
178
|
+
parser.add_argument("--page-size", type=int, default=None)
|
|
179
|
+
parser.add_argument("--page-token", default=None)
|
|
180
|
+
parser.add_argument("--batch-pages", type=int, default=None)
|
|
181
|
+
return parser
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _add_alias(parser: argparse.ArgumentParser) -> None:
|
|
185
|
+
parser.add_argument("--alias", required=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _add_exchange_inputs(parser: argparse.ArgumentParser) -> None:
|
|
189
|
+
parser.add_argument("--parameter-inputs-json", required=True, dest="parameter_inputs")
|
|
190
|
+
parser.add_argument("--user-input-json", required=True, dest="user_input")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
194
|
+
"""Build parser for 15 SDK commands and local purge."""
|
|
195
|
+
parser = _ArgumentParser(
|
|
196
|
+
prog="pal-found-aip-agents",
|
|
197
|
+
description="Foundry AIP Agents CLI - 15 SDK operations plus local purge",
|
|
198
|
+
)
|
|
199
|
+
resources = parser.add_subparsers(dest="resource")
|
|
200
|
+
|
|
201
|
+
agent = resources.add_parser("agent")
|
|
202
|
+
agent_ops = agent.add_subparsers(dest="operation")
|
|
203
|
+
agent_ops.add_parser("all-sessions", parents=[_common_parser(paged=True)])
|
|
204
|
+
agent_get = agent_ops.add_parser("get", parents=[_common_parser(paged=False)])
|
|
205
|
+
agent_get.add_argument("agent_rid")
|
|
206
|
+
agent_get.add_argument("--version")
|
|
207
|
+
|
|
208
|
+
versions = resources.add_parser("agent-version")
|
|
209
|
+
version_ops = versions.add_subparsers(dest="operation")
|
|
210
|
+
version_get = version_ops.add_parser("get", parents=[_common_parser(paged=False)])
|
|
211
|
+
version_get.add_argument("agent_rid")
|
|
212
|
+
version_get.add_argument("agent_version_string")
|
|
213
|
+
version_list = version_ops.add_parser("list", parents=[_common_parser(paged=True)])
|
|
214
|
+
version_list.add_argument("agent_rid")
|
|
215
|
+
|
|
216
|
+
sessions = resources.add_parser("session")
|
|
217
|
+
session_ops = sessions.add_subparsers(dest="operation")
|
|
218
|
+
blocking = session_ops.add_parser("blocking-continue", parents=[_common_parser(paged=False)])
|
|
219
|
+
_add_alias(blocking)
|
|
220
|
+
_add_exchange_inputs(blocking)
|
|
221
|
+
blocking.add_argument("--contexts-override-json", dest="contexts_override")
|
|
222
|
+
blocking.add_argument("--session-trace-id")
|
|
223
|
+
|
|
224
|
+
cancel = session_ops.add_parser("cancel", parents=[_common_parser(paged=False)])
|
|
225
|
+
_add_alias(cancel)
|
|
226
|
+
cancel.add_argument("--message-id", required=True)
|
|
227
|
+
cancel.add_argument("--response")
|
|
228
|
+
|
|
229
|
+
create = session_ops.add_parser("create", parents=[_common_parser(paged=False)])
|
|
230
|
+
_add_alias(create)
|
|
231
|
+
create.add_argument("--agent-rid", required=True)
|
|
232
|
+
create.add_argument("--agent-version")
|
|
233
|
+
|
|
234
|
+
for operation in ("delete", "get"):
|
|
235
|
+
operation_parser = session_ops.add_parser(operation, parents=[_common_parser(paged=False)])
|
|
236
|
+
_add_alias(operation_parser)
|
|
237
|
+
|
|
238
|
+
session_list = session_ops.add_parser("list", parents=[_common_parser(paged=True)])
|
|
239
|
+
session_list.add_argument("agent_rid")
|
|
240
|
+
|
|
241
|
+
rag = session_ops.add_parser("rag-context", parents=[_common_parser(paged=False)])
|
|
242
|
+
_add_alias(rag)
|
|
243
|
+
_add_exchange_inputs(rag)
|
|
244
|
+
|
|
245
|
+
streaming = session_ops.add_parser("streaming-continue", parents=[_common_parser(paged=False)])
|
|
246
|
+
_add_alias(streaming)
|
|
247
|
+
_add_exchange_inputs(streaming)
|
|
248
|
+
streaming.add_argument("--contexts-override-json", dest="contexts_override")
|
|
249
|
+
streaming.add_argument("--message-id")
|
|
250
|
+
streaming.add_argument("--session-trace-id")
|
|
251
|
+
streaming.add_argument("--output-filename")
|
|
252
|
+
|
|
253
|
+
title = session_ops.add_parser("update-title", parents=[_common_parser(paged=False)])
|
|
254
|
+
_add_alias(title)
|
|
255
|
+
title.add_argument("--title", required=True)
|
|
256
|
+
session_ops.add_parser("purge", parents=[_common_parser(paged=False, timeout=False)])
|
|
257
|
+
|
|
258
|
+
content = resources.add_parser("content")
|
|
259
|
+
content_ops = content.add_subparsers(dest="operation")
|
|
260
|
+
content_get = content_ops.add_parser("get", parents=[_common_parser(paged=False)])
|
|
261
|
+
_add_alias(content_get)
|
|
262
|
+
|
|
263
|
+
traces = resources.add_parser("session-trace")
|
|
264
|
+
trace_ops = traces.add_subparsers(dest="operation")
|
|
265
|
+
trace_get = trace_ops.add_parser("get", parents=[_common_parser(paged=False)])
|
|
266
|
+
_add_alias(trace_get)
|
|
267
|
+
trace_get.add_argument("--session-trace-id", required=True)
|
|
268
|
+
return parser
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _spec_for(resource: str, operation: str) -> OperationSpec:
|
|
272
|
+
"""Return one catalog operation."""
|
|
273
|
+
try:
|
|
274
|
+
return OPERATION_BY_RESOURCE[resource][operation]
|
|
275
|
+
except KeyError as exc:
|
|
276
|
+
raise ValueError(f"Unknown operation: {resource}.{operation}") from exc
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _get_client(root_client: Any, client_path: tuple[str, ...]) -> Any:
|
|
280
|
+
"""Resolve exact public SDK nested resource path."""
|
|
281
|
+
client = root_client.aip_agents
|
|
282
|
+
for attribute in client_path:
|
|
283
|
+
client = getattr(client, attribute)
|
|
284
|
+
return client
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _parse_json_object(value: str, *, field: str) -> dict[str, Any]:
|
|
288
|
+
"""Decode a required JSON object."""
|
|
289
|
+
try:
|
|
290
|
+
result = json.loads(value)
|
|
291
|
+
except json.JSONDecodeError as exc:
|
|
292
|
+
raise ValueError(f"{field} must contain valid JSON") from exc
|
|
293
|
+
if not isinstance(result, dict):
|
|
294
|
+
raise ValueError(f"{field} must be a JSON object")
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _parse_json_list(value: str, *, field: str) -> list[dict[str, Any]]:
|
|
299
|
+
"""Decode a JSON array containing only objects."""
|
|
300
|
+
try:
|
|
301
|
+
result = json.loads(value)
|
|
302
|
+
except json.JSONDecodeError as exc:
|
|
303
|
+
raise ValueError(f"{field} must contain valid JSON") from exc
|
|
304
|
+
if not isinstance(result, list) or not all(isinstance(item, dict) for item in result):
|
|
305
|
+
raise ValueError(f"{field} must be a JSON array of objects")
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _model_to_dict(value: Any) -> Any:
|
|
310
|
+
"""Convert SDK models to JSON-compatible values."""
|
|
311
|
+
if value is None:
|
|
312
|
+
return None
|
|
313
|
+
if isinstance(value, list):
|
|
314
|
+
return [_model_to_dict(item) for item in value]
|
|
315
|
+
if isinstance(value, dict):
|
|
316
|
+
return {key: _model_to_dict(item) for key, item in value.items()}
|
|
317
|
+
for method_name in ("to_dict", "model_dump", "dict"):
|
|
318
|
+
method = getattr(value, method_name, None)
|
|
319
|
+
if callable(method):
|
|
320
|
+
result = method()
|
|
321
|
+
if isinstance(result, dict):
|
|
322
|
+
return {key: _model_to_dict(item) for key, item in result.items()}
|
|
323
|
+
return value
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _required_text(value: Any, *, field: str) -> str:
|
|
327
|
+
"""Validate one non-empty scalar string."""
|
|
328
|
+
if not isinstance(value, str) or not value.strip():
|
|
329
|
+
raise ValueError(f"{field} must not be empty")
|
|
330
|
+
return value
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _validate_timeout(value: int) -> int:
|
|
334
|
+
"""Validate ADR-002 timeout range."""
|
|
335
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 3600:
|
|
336
|
+
raise ValueError("timeout must be between 1 and 3600 seconds")
|
|
337
|
+
return value
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _validate_inputs(spec: OperationSpec, args: argparse.Namespace) -> None:
|
|
341
|
+
"""Validate scalars and decode structured inputs before client creation."""
|
|
342
|
+
for name in (*spec["positional"], *spec["required"]):
|
|
343
|
+
value = getattr(args, name, None)
|
|
344
|
+
if isinstance(value, str):
|
|
345
|
+
_required_text(value, field=name)
|
|
346
|
+
for name in spec["json_objects"]:
|
|
347
|
+
value = getattr(args, name, None)
|
|
348
|
+
if value is not None:
|
|
349
|
+
setattr(args, name, _parse_json_object(value, field=name))
|
|
350
|
+
for name in spec["json_lists"]:
|
|
351
|
+
value = getattr(args, name, None)
|
|
352
|
+
if value is not None:
|
|
353
|
+
setattr(args, name, _parse_json_list(value, field=name))
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
async def _fetch_page(
|
|
357
|
+
method: Any,
|
|
358
|
+
*,
|
|
359
|
+
page_size: int,
|
|
360
|
+
page_token: str | None,
|
|
361
|
+
request_timeout: int | None,
|
|
362
|
+
call_args: tuple[Any, ...],
|
|
363
|
+
) -> dict[str, Any]:
|
|
364
|
+
"""Fetch exactly one decoded SDK response page."""
|
|
365
|
+
raw_response = await method(
|
|
366
|
+
*call_args,
|
|
367
|
+
page_size=page_size,
|
|
368
|
+
page_token=page_token,
|
|
369
|
+
request_timeout=request_timeout,
|
|
370
|
+
)
|
|
371
|
+
page = raw_response.decode()
|
|
372
|
+
return {
|
|
373
|
+
"items": list(getattr(page, "data", None) or []),
|
|
374
|
+
"next_page_token": getattr(page, "next_page_token", None),
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
async def _paginate_operation(
|
|
379
|
+
method: Any,
|
|
380
|
+
args: argparse.Namespace,
|
|
381
|
+
timeout: int | None,
|
|
382
|
+
call_args: tuple[Any, ...],
|
|
383
|
+
) -> tuple[list[Any], PaginationHelper]:
|
|
384
|
+
"""Collect a bounded number of actual server pages."""
|
|
385
|
+
helper = PaginationHelper(
|
|
386
|
+
page_size=args.page_size,
|
|
387
|
+
page_token=args.page_token,
|
|
388
|
+
batch_pages=args.batch_pages,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
async def fetch_page(**page_kwargs: Any) -> dict[str, Any]:
|
|
392
|
+
return await _fetch_page(
|
|
393
|
+
method,
|
|
394
|
+
page_size=page_kwargs["page_size"],
|
|
395
|
+
page_token=page_kwargs.get("page_token"),
|
|
396
|
+
request_timeout=timeout,
|
|
397
|
+
call_args=call_args,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
return await helper.paginate(fetch_page), helper
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
async def _one_bytes_chunk(payload: bytes) -> AsyncIterator[bytes]:
|
|
404
|
+
"""Adapt an eager SDK byte result to download-handler input."""
|
|
405
|
+
yield payload
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _load_alias(manager: SessionManager, alias: str) -> SessionState:
|
|
409
|
+
"""Load one canonical session alias."""
|
|
410
|
+
return manager.load(alias)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _record_session_use(
|
|
414
|
+
manager: SessionManager,
|
|
415
|
+
alias: str,
|
|
416
|
+
state: SessionState,
|
|
417
|
+
*,
|
|
418
|
+
operation: str,
|
|
419
|
+
succeeded: bool,
|
|
420
|
+
completed: bool = False,
|
|
421
|
+
) -> None:
|
|
422
|
+
"""Persist sanitized use history without arguments or response data."""
|
|
423
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
424
|
+
updated = SessionState(
|
|
425
|
+
session_id=state.session_id,
|
|
426
|
+
agent_rid=state.agent_rid,
|
|
427
|
+
session_token=state.session_token,
|
|
428
|
+
created_at=state.created_at,
|
|
429
|
+
last_used_at=timestamp,
|
|
430
|
+
status="completed" if completed else state.status,
|
|
431
|
+
tool_history=[
|
|
432
|
+
*state.tool_history,
|
|
433
|
+
{"timestamp": timestamp, "operation": operation, "succeeded": succeeded},
|
|
434
|
+
],
|
|
435
|
+
)
|
|
436
|
+
manager.update(alias, updated)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _sdk_call_parts(
|
|
440
|
+
spec: OperationSpec,
|
|
441
|
+
args: argparse.Namespace,
|
|
442
|
+
state: SessionState | None,
|
|
443
|
+
) -> tuple[list[Any], dict[str, Any]]:
|
|
444
|
+
"""Build documented SDK positional and keyword arguments."""
|
|
445
|
+
positional = [getattr(args, name) for name in spec["positional"]]
|
|
446
|
+
if state is not None:
|
|
447
|
+
positional = [state.agent_rid, state.session_id]
|
|
448
|
+
if spec["resource"] == "session_trace":
|
|
449
|
+
positional.append(args.session_trace_id)
|
|
450
|
+
kwargs: dict[str, Any] = {}
|
|
451
|
+
for name in (*spec["required"], *spec["optional"]):
|
|
452
|
+
if name in {"alias", "agent_rid", "output_filename", "session_trace_id"} and (
|
|
453
|
+
state is not None or name in {"alias", "output_filename"}
|
|
454
|
+
):
|
|
455
|
+
continue
|
|
456
|
+
value = getattr(args, name, None)
|
|
457
|
+
if value is not None:
|
|
458
|
+
kwargs[name] = value
|
|
459
|
+
if state is not None and "session_trace_id" in spec["optional"]:
|
|
460
|
+
value = getattr(args, "session_trace_id", None)
|
|
461
|
+
if value is not None:
|
|
462
|
+
kwargs["session_trace_id"] = value
|
|
463
|
+
return positional, kwargs
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
async def _invoke_sdk(
|
|
467
|
+
spec: OperationSpec,
|
|
468
|
+
client: Any,
|
|
469
|
+
args: argparse.Namespace,
|
|
470
|
+
timeout: int,
|
|
471
|
+
cfg: ConfigLoader,
|
|
472
|
+
state: SessionState | None,
|
|
473
|
+
) -> tuple[Any, PaginationHelper | None]:
|
|
474
|
+
"""Invoke one complete SDK operation attempt."""
|
|
475
|
+
positional, kwargs = _sdk_call_parts(spec, args, state)
|
|
476
|
+
if (spec["resource"], spec["operation"]) in _PAGED:
|
|
477
|
+
raw_method = getattr(client.with_raw_response, spec["method"])
|
|
478
|
+
result, helper = await _paginate_operation(
|
|
479
|
+
raw_method, args, timeout, tuple(positional)
|
|
480
|
+
)
|
|
481
|
+
else:
|
|
482
|
+
kwargs["request_timeout"] = timeout
|
|
483
|
+
result = await getattr(client, spec["method"])(*positional, **kwargs)
|
|
484
|
+
helper = None
|
|
485
|
+
if spec["operation"] == "streaming_continue":
|
|
486
|
+
if not isinstance(result, bytes):
|
|
487
|
+
raise SDKContractError("streaming_continue returned an invalid SDK result")
|
|
488
|
+
download = await BinaryDownloadHandler(config=cfg).save(
|
|
489
|
+
_one_bytes_chunk(result),
|
|
490
|
+
original_filename=args.output_filename,
|
|
491
|
+
namespace="aip_agents",
|
|
492
|
+
operation="session.streaming_continue",
|
|
493
|
+
content_length=None,
|
|
494
|
+
content_encoding=None,
|
|
495
|
+
mime_type=None,
|
|
496
|
+
)
|
|
497
|
+
result = download.to_dict()
|
|
498
|
+
return result, helper
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
async def _create_session(
|
|
502
|
+
client: Any,
|
|
503
|
+
args: argparse.Namespace,
|
|
504
|
+
timeout: int,
|
|
505
|
+
manager: SessionManager,
|
|
506
|
+
) -> dict[str, Any]:
|
|
507
|
+
"""Create remote session and publish its local alias atomically."""
|
|
508
|
+
retry = RetryHandler(timeout_s=timeout)
|
|
509
|
+
|
|
510
|
+
async def create_remote() -> Any:
|
|
511
|
+
create_kwargs: dict[str, Any] = {"request_timeout": timeout}
|
|
512
|
+
if args.agent_version is not None:
|
|
513
|
+
create_kwargs["agent_version"] = args.agent_version
|
|
514
|
+
return await retry.execute(
|
|
515
|
+
client.create,
|
|
516
|
+
args.agent_rid,
|
|
517
|
+
**create_kwargs,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
async def delete_remote(session_id: str) -> None:
|
|
521
|
+
await client.delete(
|
|
522
|
+
args.agent_rid,
|
|
523
|
+
session_id,
|
|
524
|
+
request_timeout=timeout,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
state = await manager.create(
|
|
528
|
+
args.alias, args.agent_rid, create_remote, delete_remote
|
|
529
|
+
)
|
|
530
|
+
result = state.to_dict()
|
|
531
|
+
result.pop("session_token", None)
|
|
532
|
+
result["alias"] = SessionManager.normalize_alias(args.alias)
|
|
533
|
+
return result
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _serialize_error(exception: BaseException) -> int:
|
|
537
|
+
"""Write one structured failure and return its ADR-001 exit code."""
|
|
538
|
+
serializer = ErrorSerializer()
|
|
539
|
+
status = sdk_http_status(exception)
|
|
540
|
+
declared = getattr(exception, "exit_code", None)
|
|
541
|
+
sdk_exit_code = sdk_exception_exit_code(exception)
|
|
542
|
+
if isinstance(declared, int) and 1 <= declared <= 9:
|
|
543
|
+
exit_code = declared
|
|
544
|
+
elif status == 401:
|
|
545
|
+
exit_code = EXIT_AUTH
|
|
546
|
+
elif status == 403:
|
|
547
|
+
exit_code = EXIT_PERMISSION_DENIED
|
|
548
|
+
elif status == 404:
|
|
549
|
+
exit_code = EXIT_NOT_FOUND
|
|
550
|
+
elif status == 409:
|
|
551
|
+
exit_code = EXIT_USER_INPUT
|
|
552
|
+
elif status == 429:
|
|
553
|
+
exit_code = EXIT_RATE_LIMIT
|
|
554
|
+
elif sdk_exit_code is not None:
|
|
555
|
+
exit_code = sdk_exit_code
|
|
556
|
+
elif isinstance(exception, ConfigurationError):
|
|
557
|
+
exit_code = EXIT_CONFIGURATION
|
|
558
|
+
elif isinstance(exception, (TypeError, ValueError)):
|
|
559
|
+
exit_code = EXIT_USER_INPUT
|
|
560
|
+
elif isinstance(exception, PermissionError):
|
|
561
|
+
exit_code = EXIT_PERMISSION_DENIED
|
|
562
|
+
elif isinstance(exception, FileNotFoundError):
|
|
563
|
+
exit_code = EXIT_NOT_FOUND
|
|
564
|
+
elif isinstance(exception, TimeoutError):
|
|
565
|
+
exit_code = EXIT_TIMEOUT
|
|
566
|
+
else:
|
|
567
|
+
exit_code = EXIT_SERVER_ERROR
|
|
568
|
+
|
|
569
|
+
expose_message = isinstance(
|
|
570
|
+
exception,
|
|
571
|
+
(ConfigurationError, FileNotFoundError, PermissionError, TimeoutError, TypeError, ValueError),
|
|
572
|
+
) or declared in {
|
|
573
|
+
EXIT_USER_INPUT,
|
|
574
|
+
EXIT_NOT_FOUND,
|
|
575
|
+
EXIT_ACCESS_CONTROL,
|
|
576
|
+
EXIT_CONFIGURATION,
|
|
577
|
+
}
|
|
578
|
+
message = str(exception) if expose_message else "AIP Agents operation failed"
|
|
579
|
+
envelope = serializer.create_error_envelope(
|
|
580
|
+
exit_code, message, type(exception).__name__, serializer.call_id
|
|
581
|
+
)
|
|
582
|
+
if isinstance(status, int):
|
|
583
|
+
envelope["http_status"] = status
|
|
584
|
+
sys.stdout.write(json.dumps(envelope, default=str) + "\n")
|
|
585
|
+
sys.stdout.flush()
|
|
586
|
+
logger.error(
|
|
587
|
+
"AIP Agents operation failed",
|
|
588
|
+
extra={
|
|
589
|
+
"call_id": serializer.call_id,
|
|
590
|
+
"exception_type": type(exception).__name__,
|
|
591
|
+
"http_status": status,
|
|
592
|
+
},
|
|
593
|
+
)
|
|
594
|
+
return exit_code
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
async def main() -> int:
|
|
598
|
+
"""Run one stateless Foundry AIP Agents invocation."""
|
|
599
|
+
parser = build_parser()
|
|
600
|
+
try:
|
|
601
|
+
args = parser.parse_args()
|
|
602
|
+
if not args.resource or not getattr(args, "operation", None):
|
|
603
|
+
raise ValueError("an AIP Agents operation is required")
|
|
604
|
+
except ValueError as exc:
|
|
605
|
+
return _serialize_error(exc)
|
|
606
|
+
|
|
607
|
+
try:
|
|
608
|
+
cfg = ConfigLoader()
|
|
609
|
+
cfg.load()
|
|
610
|
+
LogSetup.configure(log_level=cfg.log_level)
|
|
611
|
+
manager = SessionManager(config=cfg)
|
|
612
|
+
manager.cleanup_expired()
|
|
613
|
+
|
|
614
|
+
resource = args.resource.replace("-", "_")
|
|
615
|
+
operation = args.operation.replace("-", "_")
|
|
616
|
+
is_purge = (resource, operation) == ("session", "purge")
|
|
617
|
+
spec = None if is_purge else _spec_for(resource, operation)
|
|
618
|
+
if spec is not None:
|
|
619
|
+
_validate_inputs(spec, args)
|
|
620
|
+
|
|
621
|
+
guard = AccessControlGuard(
|
|
622
|
+
cfg,
|
|
623
|
+
"AIP_AGENTS",
|
|
624
|
+
metadata_allowlist_path=str(_METADATA_ALLOWLIST_PATH),
|
|
625
|
+
)
|
|
626
|
+
guard.check(resource, operation)
|
|
627
|
+
if is_purge:
|
|
628
|
+
result: Any = {"purged_sessions": manager.purge()}
|
|
629
|
+
print(OutputFormatter(format_setting="json", pretty=args.pretty).format(result))
|
|
630
|
+
return EXIT_SUCCESS
|
|
631
|
+
|
|
632
|
+
timeout = _validate_timeout(args.timeout if args.timeout is not None else cfg.timeout_s)
|
|
633
|
+
state = (
|
|
634
|
+
_load_alias(manager, args.alias)
|
|
635
|
+
if (resource, operation) in _ALIAS_BOUND
|
|
636
|
+
else None
|
|
637
|
+
)
|
|
638
|
+
factory = AsyncClientFactory()
|
|
639
|
+
helper: PaginationHelper | None = None
|
|
640
|
+
if spec is None:
|
|
641
|
+
raise ValueError(f"Unknown operation: {resource}.{operation}")
|
|
642
|
+
with factory.invocation_scope(cfg, include_attribution=False):
|
|
643
|
+
root_client = factory.create(cfg, include_attribution=False)
|
|
644
|
+
client = _get_client(root_client, spec["client_path"])
|
|
645
|
+
if (resource, operation) == ("session", "create"):
|
|
646
|
+
result = await _create_session(client, args, timeout, manager)
|
|
647
|
+
else:
|
|
648
|
+
retry = RetryHandler(timeout_s=timeout)
|
|
649
|
+
try:
|
|
650
|
+
result, helper = await retry.execute(
|
|
651
|
+
_invoke_sdk, spec, client, args, timeout, cfg, state
|
|
652
|
+
)
|
|
653
|
+
if state is not None:
|
|
654
|
+
_record_session_use(
|
|
655
|
+
manager,
|
|
656
|
+
args.alias,
|
|
657
|
+
state,
|
|
658
|
+
operation=f"{resource}.{operation}",
|
|
659
|
+
succeeded=True,
|
|
660
|
+
completed=operation == "delete",
|
|
661
|
+
)
|
|
662
|
+
except Exception:
|
|
663
|
+
if state is not None:
|
|
664
|
+
try:
|
|
665
|
+
_record_session_use(
|
|
666
|
+
manager,
|
|
667
|
+
args.alias,
|
|
668
|
+
state,
|
|
669
|
+
operation=f"{resource}.{operation}",
|
|
670
|
+
succeeded=False,
|
|
671
|
+
)
|
|
672
|
+
except Exception:
|
|
673
|
+
logger.warning(
|
|
674
|
+
"Could not persist failed session use",
|
|
675
|
+
extra={
|
|
676
|
+
"session_alias": SessionManager.normalize_alias(
|
|
677
|
+
args.alias
|
|
678
|
+
)
|
|
679
|
+
},
|
|
680
|
+
)
|
|
681
|
+
raise
|
|
682
|
+
|
|
683
|
+
force_json = operation == "streaming_continue"
|
|
684
|
+
formatter = OutputFormatter(
|
|
685
|
+
format_setting="json" if force_json else args.format,
|
|
686
|
+
pretty=args.pretty,
|
|
687
|
+
)
|
|
688
|
+
print(formatter.format(_model_to_dict(result)))
|
|
689
|
+
if helper is not None:
|
|
690
|
+
helper.emit_metadata()
|
|
691
|
+
return EXIT_SUCCESS
|
|
692
|
+
except asyncio.CancelledError:
|
|
693
|
+
return _serialize_error(TimeoutError("Operation cancelled"))
|
|
694
|
+
except Exception as exc:
|
|
695
|
+
return _serialize_error(exc)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def console_main() -> int:
|
|
699
|
+
"""Run async CLI through one event-loop boundary."""
|
|
700
|
+
return asyncio.run(main())
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
if __name__ == "__main__":
|
|
704
|
+
raise SystemExit(console_main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Foundry Audit CLI package."""
|