deepflow-openagent-cli 0.1.6__tar.gz → 0.1.7__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.
Files changed (15) hide show
  1. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/PKG-INFO +28 -3
  2. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/README.md +27 -2
  3. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/pyproject.toml +1 -1
  4. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/src/deepflow_openagent_cli/__init__.py +1 -1
  5. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/src/deepflow_openagent_cli/cli.py +68 -0
  6. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/src/deepflow_openagent_cli.egg-info/PKG-INFO +28 -3
  7. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/tests/test_cloudpc_cli.py +80 -0
  8. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/setup.cfg +0 -0
  9. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/src/deepflow_openagent_cli/__main__.py +0 -0
  10. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/src/deepflow_openagent_cli.egg-info/SOURCES.txt +0 -0
  11. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/src/deepflow_openagent_cli.egg-info/dependency_links.txt +0 -0
  12. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/src/deepflow_openagent_cli.egg-info/entry_points.txt +0 -0
  13. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/src/deepflow_openagent_cli.egg-info/requires.txt +0 -0
  14. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/src/deepflow_openagent_cli.egg-info/top_level.txt +0 -0
  15. {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.7}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepflow-openagent-cli
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Command-line client for OpenAgent OpenAPI (agents, conversations, SSE chat, tools, channels, knowledge bases).
5
5
  Author: DeepFlow AI
6
6
  Project-URL: Repository, https://github.com/DeepFlowAI/OpenAgent
@@ -158,6 +158,30 @@ selected version does not exist; use `--from base` for a fresh environment.
158
158
  Version ID is different from the `x.y.z` version label. First save automatically
159
159
  publishes; later saves require a separate `versions publish` command.
160
160
 
161
+ Binary uploads (including PDF) use the same sandbox upload writer as the workbench:
162
+
163
+ ```bash
164
+ deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf
165
+ # After retry exhaustion, reuse BOTH printed IDs and the same local file:
166
+ deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf \
167
+ --upload-id UPLOAD_ID --sandbox-id SANDBOX_ID
168
+ ```
169
+
170
+ Uploads accept 1 byte–50 MiB and create `/workspace/files/uploads` automatically.
171
+ The returned path is `/workspace/files/uploads/<upload_id>-<original_filename>`;
172
+ use that path in subsequent OCR or chat commands. The ID prefix prevents duplicate
173
+ files after a lost response. Reusing the same ID/name with different bytes is
174
+ rejected. The sandbox ID pins retries to the original development environment.
175
+ This admin/config API accepts binary content; channel visitor format restrictions
176
+ still apply only to visitor uploads. Uploading does not automatically start OCR.
177
+
178
+ Uploads and `dev exec` submission/status/stop retry connection failures and HTTP
179
+ 408/429/502/503/504 up to four times (1/2/4/8 second delays). Execution retries keep
180
+ the same execution ID and output cursor. Upload retries resend the whole binary
181
+ body; chunk resume is not implemented. Other mutating commands are not retried.
182
+ Requires updated CLI, API server and Cloud PC control plane; no VM image rebuild
183
+ is needed for the upload writer.
184
+
161
185
  Additional operations:
162
186
 
163
187
  ```bash
@@ -176,8 +200,9 @@ Config update replaces configuration: provide `model_name`, `thinking_enabled`
176
200
  and the complete `env_vars` list; omitted thinking/env fields reset to defaults.
177
201
  Config queries include environment variable values. File mutation and snapshot
178
202
  operations share the per-agent management lock; direct writes by the sandbox's
179
- Agent or terminal are outside that lock. Cloud PC HTTP requests have a 900-second
180
- timeout and are not automatically retried. After a timeout, inspect sandbox or
203
+ Agent or terminal are outside that lock. Upload and execution requests have a
204
+ 120-second per-attempt timeout and the retries described above. Other Cloud PC
205
+ HTTP requests have a 900-second timeout and are not automatically retried. After a timeout, inspect sandbox or
181
206
  version state before retrying a mutation. Conversation logs and chat-control
182
207
  routes still require an administrator session.
183
208
 
@@ -137,6 +137,30 @@ selected version does not exist; use `--from base` for a fresh environment.
137
137
  Version ID is different from the `x.y.z` version label. First save automatically
138
138
  publishes; later saves require a separate `versions publish` command.
139
139
 
140
+ Binary uploads (including PDF) use the same sandbox upload writer as the workbench:
141
+
142
+ ```bash
143
+ deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf
144
+ # After retry exhaustion, reuse BOTH printed IDs and the same local file:
145
+ deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf \
146
+ --upload-id UPLOAD_ID --sandbox-id SANDBOX_ID
147
+ ```
148
+
149
+ Uploads accept 1 byte–50 MiB and create `/workspace/files/uploads` automatically.
150
+ The returned path is `/workspace/files/uploads/<upload_id>-<original_filename>`;
151
+ use that path in subsequent OCR or chat commands. The ID prefix prevents duplicate
152
+ files after a lost response. Reusing the same ID/name with different bytes is
153
+ rejected. The sandbox ID pins retries to the original development environment.
154
+ This admin/config API accepts binary content; channel visitor format restrictions
155
+ still apply only to visitor uploads. Uploading does not automatically start OCR.
156
+
157
+ Uploads and `dev exec` submission/status/stop retry connection failures and HTTP
158
+ 408/429/502/503/504 up to four times (1/2/4/8 second delays). Execution retries keep
159
+ the same execution ID and output cursor. Upload retries resend the whole binary
160
+ body; chunk resume is not implemented. Other mutating commands are not retried.
161
+ Requires updated CLI, API server and Cloud PC control plane; no VM image rebuild
162
+ is needed for the upload writer.
163
+
140
164
  Additional operations:
141
165
 
142
166
  ```bash
@@ -155,8 +179,9 @@ Config update replaces configuration: provide `model_name`, `thinking_enabled`
155
179
  and the complete `env_vars` list; omitted thinking/env fields reset to defaults.
156
180
  Config queries include environment variable values. File mutation and snapshot
157
181
  operations share the per-agent management lock; direct writes by the sandbox's
158
- Agent or terminal are outside that lock. Cloud PC HTTP requests have a 900-second
159
- timeout and are not automatically retried. After a timeout, inspect sandbox or
182
+ Agent or terminal are outside that lock. Upload and execution requests have a
183
+ 120-second per-attempt timeout and the retries described above. Other Cloud PC
184
+ HTTP requests have a 900-second timeout and are not automatically retried. After a timeout, inspect sandbox or
160
185
  version state before retrying a mutation. Conversation logs and chat-control
161
186
  routes still require an administrator session.
162
187
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepflow-openagent-cli"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "Command-line client for OpenAgent OpenAPI (agents, conversations, SSE chat, tools, channels, knowledge bases)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """DeepFlow OpenAgent CLI — published PyPI package."""
2
2
 
3
- __version__ = "0.1.6"
3
+ __version__ = "0.1.7"
@@ -8,6 +8,7 @@ Run with:
8
8
  from __future__ import annotations
9
9
 
10
10
  import argparse
11
+ from http.client import HTTPException
11
12
  import json
12
13
  import mimetypes
13
14
  import os
@@ -129,6 +130,18 @@ def _param(name: str, type_: str, required: bool, description: str) -> ParamDoc:
129
130
 
130
131
 
131
132
  ENDPOINTS: tuple[EndpointDoc, ...] = (
133
+ EndpointDoc(
134
+ key="cloudpc.files.upload", method="POST",
135
+ path="/api/v1/cloudpc/agents/{agent_id}/dev-sandbox/files/upload", scope="config",
136
+ summary="Upload binary content (1 byte–50 MiB) into /workspace/files/uploads; safe to retry with the same upload ID and sandbox ID.",
137
+ path_params=[_param("agent_id", "integer", True, "Cloud PC Agent ID.")],
138
+ query_params=[_param("filename", "string", True, "Original basename."),
139
+ _param("upload_id", "string", True, "32 lowercase hex characters; prefixes the saved filename."),
140
+ _param("sandbox_id", "integer", True, "Pinned development sandbox ID from dev get.")],
141
+ response="Saved filename, absolute path and size.",
142
+ example="deepflow_openagent_cli cloudpc files upload 1 --file ./report.pdf",
143
+ ),
144
+
132
145
  EndpointDoc(
133
146
  key="cloudpc.dev.exec", method="POST", path="/api/v1/cloudpc/agents/{agent_id}/dev-sandbox/executions", scope="config",
134
147
  summary='Start an idempotent shell execution. Output remains in the sandbox runtime until restart or eviction (32 retained).',
@@ -1254,6 +1267,38 @@ class OpenApiClient:
1254
1267
  except (URLError, OSError, UnicodeError, ValueError) as exc:
1255
1268
  raise CliError(f"Chat stream interrupted: {exc}") from exc
1256
1269
 
1270
+ def _read_retryable(self, request: Request) -> bytes:
1271
+ """Only for operations whose server contract makes replay safe."""
1272
+ for attempt in range(5):
1273
+ try:
1274
+ with urlopen(request, timeout=120) as response:
1275
+ return response.read()
1276
+ except HTTPError as exc:
1277
+ if exc.code not in {408, 429, 502, 503, 504} or attempt == 4:
1278
+ raise CliError(f"HTTP {exc.code}: {exc.read().decode('utf-8', errors='replace')}") from exc
1279
+ except (URLError, OSError, HTTPException) as exc:
1280
+ if attempt == 4:
1281
+ raise CliError(f"Connection failed after 5 attempts: {exc}") from exc
1282
+ print(f"Connection interrupted; retry {attempt + 1}/4…", file=sys.stderr)
1283
+ time.sleep(2 ** attempt)
1284
+
1285
+ def upload(self, agent_id: int, file: Path, upload_id: str, sandbox_id: int) -> bytes:
1286
+ if not self.api_key:
1287
+ raise CliError(f"Missing API key. Set {ENV_API_KEY} or pass --api-key.")
1288
+ try:
1289
+ with file.open("rb") as source:
1290
+ content = source.read(50 * 1024 * 1024 + 1)
1291
+ except OSError as exc:
1292
+ raise CliError(f"Cannot read upload: {exc}") from exc
1293
+ if not 0 < len(content) <= 50 * 1024 * 1024:
1294
+ raise CliError("Upload must be between 1 byte and 50 MiB.")
1295
+ url = self._build_url(get_endpoint("cloudpc.files.upload"), {"agent_id": agent_id},
1296
+ {"filename": file.name, "upload_id": upload_id, "sandbox_id": sandbox_id})
1297
+ request = Request(url, data=content, method="POST", headers={
1298
+ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/octet-stream",
1299
+ })
1300
+ return self._read_retryable(request)
1301
+
1257
1302
  def request(
1258
1303
  self,
1259
1304
  endpoint_key: str,
@@ -1262,6 +1307,7 @@ class OpenApiClient:
1262
1307
  body: dict[str, Any] | None = None,
1263
1308
  *,
1264
1309
  stream: bool = False,
1310
+ retry: bool = False,
1265
1311
  ) -> bytes | None:
1266
1312
  endpoint = get_endpoint(endpoint_key)
1267
1313
  if endpoint.scope and not self.api_key:
@@ -1277,6 +1323,8 @@ class OpenApiClient:
1277
1323
  headers["Content-Type"] = "application/json"
1278
1324
 
1279
1325
  request = Request(url, data=data, headers=headers, method=endpoint.method)
1326
+ if not stream and (retry or endpoint_key in {"cloudpc.dev.exec", "cloudpc.dev.exec_status", "cloudpc.dev.exec_cancel"}):
1327
+ return self._read_retryable(request)
1280
1328
  try:
1281
1329
  options = {"timeout": 900} if endpoint_key.startswith("cloudpc.") else {}
1282
1330
  with urlopen(request, **options) as response: # noqa: S310 - CLI calls user-provided API host.
@@ -1919,6 +1967,21 @@ def handle_cloudpc(args: argparse.Namespace) -> int:
1919
1967
  raise CliError("Version ID must be positive.")
1920
1968
  body = {"from_version_id": version_id}
1921
1969
  elif group == "files":
1970
+ if action == "upload":
1971
+ if bool(args.upload_id) != (args.sandbox_id is not None):
1972
+ raise CliError("Use --upload-id and --sandbox-id together when resuming an upload.")
1973
+ client = client_from_args(args)
1974
+ upload_id = args.upload_id or uuid4().hex
1975
+ sandbox_id = args.sandbox_id
1976
+ if sandbox_id is None:
1977
+ dev = json.loads(client.request("cloudpc.dev.get", path_params, retry=True))
1978
+ sandbox_id = dev.get("sandbox_id")
1979
+ if sandbox_id is None:
1980
+ raise CliError("No upload-capable development sandbox; create/resume it and update the server.")
1981
+ print(f"upload_id={upload_id} sandbox_id={sandbox_id}; reuse both IDs to retry this upload.", file=sys.stderr)
1982
+ data = client.upload(args.agent_id, Path(args.file).expanduser(), upload_id, sandbox_id)
1983
+ print_output(data, pretty=args.pretty, raw=args.raw)
1984
+ return 0
1922
1985
  if action in {"read", "delete"}:
1923
1986
  query = {"path": args.path}
1924
1987
  elif action == "write":
@@ -2129,6 +2192,11 @@ def build_parser() -> argparse.ArgumentParser:
2129
2192
  elif action == "rename":
2130
2193
  command.add_argument("--src", required=True)
2131
2194
  command.add_argument("--dst", required=True)
2195
+ upload = cloudpc_files.add_parser("upload", help="Upload binary files to /workspace/files/uploads (max 50 MiB).")
2196
+ upload.add_argument("agent_id", type=int)
2197
+ upload.add_argument("--file", required=True)
2198
+ upload.add_argument("--upload-id", help="Reuse the printed ID when retrying an interrupted upload.")
2199
+ upload.add_argument("--sandbox-id", type=int, help="Reuse the printed sandbox ID on manual retry.")
2132
2200
  cloudpc_versions = cloudpc_sub.add_parser("versions").add_subparsers(dest="cloudpc_action", required=True)
2133
2201
  for action in ("list", "save", "publish"):
2134
2202
  command = cloudpc_versions.add_parser(action)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepflow-openagent-cli
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Command-line client for OpenAgent OpenAPI (agents, conversations, SSE chat, tools, channels, knowledge bases).
5
5
  Author: DeepFlow AI
6
6
  Project-URL: Repository, https://github.com/DeepFlowAI/OpenAgent
@@ -158,6 +158,30 @@ selected version does not exist; use `--from base` for a fresh environment.
158
158
  Version ID is different from the `x.y.z` version label. First save automatically
159
159
  publishes; later saves require a separate `versions publish` command.
160
160
 
161
+ Binary uploads (including PDF) use the same sandbox upload writer as the workbench:
162
+
163
+ ```bash
164
+ deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf
165
+ # After retry exhaustion, reuse BOTH printed IDs and the same local file:
166
+ deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf \
167
+ --upload-id UPLOAD_ID --sandbox-id SANDBOX_ID
168
+ ```
169
+
170
+ Uploads accept 1 byte–50 MiB and create `/workspace/files/uploads` automatically.
171
+ The returned path is `/workspace/files/uploads/<upload_id>-<original_filename>`;
172
+ use that path in subsequent OCR or chat commands. The ID prefix prevents duplicate
173
+ files after a lost response. Reusing the same ID/name with different bytes is
174
+ rejected. The sandbox ID pins retries to the original development environment.
175
+ This admin/config API accepts binary content; channel visitor format restrictions
176
+ still apply only to visitor uploads. Uploading does not automatically start OCR.
177
+
178
+ Uploads and `dev exec` submission/status/stop retry connection failures and HTTP
179
+ 408/429/502/503/504 up to four times (1/2/4/8 second delays). Execution retries keep
180
+ the same execution ID and output cursor. Upload retries resend the whole binary
181
+ body; chunk resume is not implemented. Other mutating commands are not retried.
182
+ Requires updated CLI, API server and Cloud PC control plane; no VM image rebuild
183
+ is needed for the upload writer.
184
+
161
185
  Additional operations:
162
186
 
163
187
  ```bash
@@ -176,8 +200,9 @@ Config update replaces configuration: provide `model_name`, `thinking_enabled`
176
200
  and the complete `env_vars` list; omitted thinking/env fields reset to defaults.
177
201
  Config queries include environment variable values. File mutation and snapshot
178
202
  operations share the per-agent management lock; direct writes by the sandbox's
179
- Agent or terminal are outside that lock. Cloud PC HTTP requests have a 900-second
180
- timeout and are not automatically retried. After a timeout, inspect sandbox or
203
+ Agent or terminal are outside that lock. Upload and execution requests have a
204
+ 120-second per-attempt timeout and the retries described above. Other Cloud PC
205
+ HTTP requests have a 900-second timeout and are not automatically retried. After a timeout, inspect sandbox or
181
206
  version state before retrying a mutation. Conversation logs and chat-control
182
207
  routes still require an administrator session.
183
208
 
@@ -254,3 +254,83 @@ def test_dev_chat_requires_message_or_existing_round(cli, transport):
254
254
  assert run(cli, 'cloudpc', 'dev', 'chat', '7') == 1
255
255
  assert run(cli, 'cloudpc', 'dev', 'chat', '7', '--attach') == 1
256
256
  assert transport[0] == []
257
+
258
+
259
+ def test_binary_upload_replays_exact_body_after_ssl_eof(cli, monkeypatch, tmp_path, capsys):
260
+ import ssl
261
+ source = tmp_path / '报告.pdf'
262
+ content = b'%PDF-1.7\n\x00\xff' + b'x' * (2 * 1024 * 1024)
263
+ source.write_bytes(content)
264
+ calls = []
265
+ def send(request, **kwargs):
266
+ calls.append(request)
267
+ if request.method == 'GET':
268
+ return io.BytesIO(b'{"sandbox_id":42,"exists":true}')
269
+ if len(calls) == 2:
270
+ raise ssl.SSLEOFError('UNEXPECTED_EOF_WHILE_READING')
271
+ return io.BytesIO(b'{"path":"/workspace/files/uploads/saved.pdf"}')
272
+ monkeypatch.setattr(cli, 'urlopen', send)
273
+ monkeypatch.setattr(cli.time, 'sleep', lambda _: None)
274
+ assert run(cli, 'cloudpc', 'files', 'upload', '7', '--file', str(source)) == 0
275
+ assert calls[1].data == calls[2].data == content
276
+ assert calls[1].full_url == calls[2].full_url
277
+ assert 'sandbox_id=42' in calls[1].full_url
278
+ assert 'upload_id=' in capsys.readouterr().err
279
+
280
+
281
+ def test_exec_retries_same_submission_and_cursor(cli, monkeypatch, capsys):
282
+ from http.client import IncompleteRead
283
+ calls = []
284
+ def send(request, **kwargs):
285
+ calls.append(request)
286
+ if len(calls) in {1, 3}:
287
+ raise IncompleteRead(b'partial')
288
+ if request.method == 'POST':
289
+ return io.BytesIO(b'{}')
290
+ return io.BytesIO(b'{"events":[{"event":{"type":"stdout","text":"once"}}],"cursor":1,"state":"exited","exit_code":0}')
291
+ monkeypatch.setattr(cli, 'urlopen', send)
292
+ monkeypatch.setattr(cli.time, 'sleep', lambda _: None)
293
+ assert run(cli, 'cloudpc', 'dev', 'exec', '7', '--command', 'echo once', '--execution-id', 'job') == 0
294
+ assert calls[0].data == calls[1].data
295
+ assert calls[2].full_url == calls[3].full_url
296
+ assert capsys.readouterr().out == 'once'
297
+
298
+
299
+ @pytest.mark.parametrize('code', [400, 401, 403, 409, 413])
300
+ def test_upload_permanent_errors_not_retried(cli, monkeypatch, tmp_path, code):
301
+ file = tmp_path / 'x.pdf'
302
+ file.write_bytes(b'\xff\x00')
303
+ calls = []
304
+ def send(request, **kwargs):
305
+ calls.append(request)
306
+ raise HTTPError(request.full_url, code, 'rejected', {}, io.BytesIO(b'rejected'))
307
+ monkeypatch.setattr(cli, 'urlopen', send)
308
+ assert run(cli, 'cloudpc', 'files', 'upload', '7', '--file', str(file), '--sandbox-id', '42', '--upload-id', 'a' * 32) == 1
309
+ assert len(calls) == 1
310
+
311
+
312
+ def test_exec_retry_exhaustion_preserves_attach_hint(cli, monkeypatch, capsys):
313
+ calls = []
314
+ def send(request, **kwargs):
315
+ calls.append(request)
316
+ raise ConnectionResetError('reset')
317
+ monkeypatch.setattr(cli, 'urlopen', send)
318
+ monkeypatch.setattr(cli.time, 'sleep', lambda _: None)
319
+ assert run(cli, 'cloudpc', 'dev', 'exec-attach', '7', 'job', '--after', '8') == 1
320
+ assert len(calls) == 5
321
+ assert all('after=8' in request.full_url for request in calls)
322
+ assert 'exec-attach 7 job --after 8' in capsys.readouterr().err
323
+
324
+
325
+ @pytest.mark.parametrize('size', [0, 50 * 1024 * 1024 + 1])
326
+ def test_upload_size_rejected_before_send(cli, transport, tmp_path, size):
327
+ source = tmp_path / 'x.pdf'
328
+ with source.open('wb') as file:
329
+ file.truncate(size)
330
+ assert run(cli, 'cloudpc', 'files', 'upload', '7', '--file', str(source), '--upload-id', 'a' * 32, '--sandbox-id', '42') == 1
331
+ assert transport[0] == []
332
+
333
+
334
+ def test_upload_manual_retry_requires_both_ids(cli, transport):
335
+ assert run(cli, 'cloudpc', 'files', 'upload', '7', '--file', 'x.pdf', '--upload-id', 'a' * 32) == 1
336
+ assert transport[0] == []