keystone-cli 0.1.10__tar.gz → 0.1.26__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.
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/PKG-INFO +2 -2
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/pyproject.toml +3 -3
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/__main__.py +10 -0
- keystone_cli-0.1.26/src/keystone_cli/_stdio.py +68 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/auth/token_store.py +53 -5
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/commands/agent.py +345 -11
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/commands/dev.py +1 -0
- keystone_cli-0.1.26/src/keystone_cli/commands/kb.py +144 -0
- keystone_cli-0.1.26/src/keystone_cli/commands/tool_credentials.py +153 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/devloop.py +38 -6
- keystone_cli-0.1.26/src/keystone_cli/runner.py +595 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/scaffold.py +32 -7
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/agent.yaml.tmpl +3 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/guardrails/README.md.tmpl +63 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/guardrails/agent.yaml.tmpl +53 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/guardrails/pkg/graph.py.tmpl +102 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/README.md.tmpl +13 -1
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/agent.yaml.tmpl +8 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/pkg/graph.py.tmpl +13 -13
- keystone_cli-0.1.26/src/keystone_cli/templates/kb-builder/README.md.tmpl +23 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/kb-builder/agent.yaml.tmpl +45 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/kb-builder/pkg/graph.py.tmpl +73 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/llm/agent.yaml.tmpl +8 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/llm/pkg/graph.py.tmpl +5 -9
- keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming/README.md.tmpl +46 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming/agent.yaml.tmpl +39 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming/pkg/graph.py.tmpl +53 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming/pyproject.toml.tmpl +13 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory/agent.yaml.tmpl +3 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory/env.tmpl +10 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory/gitignore.tmpl +7 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory/pkg/__init__.py.tmpl +0 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory/pyproject.toml.tmpl +12 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory-service/README.md.tmpl +70 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory-service/agent.yaml.tmpl +36 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory-service/env.tmpl +10 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory-service/gitignore.tmpl +7 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory-service/pkg/__init__.py.tmpl +0 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory-service/pkg/graph.py.tmpl +98 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/memory-service/pyproject.toml.tmpl +12 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/rag-qa/agent.yaml.tmpl +61 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/rag-qa/env.tmpl +10 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/rag-qa/gitignore.tmpl +7 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/rag-qa/pkg/__init__.py.tmpl +0 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/rag-qa/pkg/graph.py.tmpl +73 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/rag-qa/pyproject.toml.tmpl +12 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/skill/README.md.tmpl +52 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/skill/agent.yaml.tmpl +30 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/skill/env.tmpl +10 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/skill/gitignore.tmpl +7 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/skill/pkg/__init__.py.tmpl +0 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/skill/pkg/graph.py.tmpl +82 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/skill/pyproject.toml.tmpl +12 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/tool-agent/agent.yaml.tmpl +8 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/tool-agent/env.tmpl +10 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/tool-agent/gitignore.tmpl +7 -0
- keystone_cli-0.1.26/src/keystone_cli/templates/tool-agent/pkg/__init__.py.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/tool-agent/pkg/graph.py.tmpl +24 -16
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/tool-agent/pyproject.toml.tmpl +3 -1
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/PKG-INFO +2 -2
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/SOURCES.txt +43 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/requires.txt +1 -1
- keystone_cli-0.1.26/tests/test_assistant_provenance_and_flags.py +180 -0
- keystone_cli-0.1.26/tests/test_auth.py +238 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_deploy.py +78 -1
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_dev.py +1 -1
- keystone_cli-0.1.26/tests/test_doctor.py +195 -0
- keystone_cli-0.1.26/tests/test_encoding.py +131 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_init.py +65 -1
- keystone_cli-0.1.26/tests/test_offline.py +348 -0
- keystone_cli-0.1.26/tests/test_stdio.py +90 -0
- keystone_cli-0.1.26/tests/test_tool_credentials.py +94 -0
- keystone_cli-0.1.10/src/keystone_cli/runner.py +0 -87
- keystone_cli-0.1.10/src/keystone_cli/templates/rag-qa/agent.yaml.tmpl +0 -34
- keystone_cli-0.1.10/src/keystone_cli/templates/rag-qa/pkg/graph.py.tmpl +0 -69
- keystone_cli-0.1.10/tests/test_auth.py +0 -118
- keystone_cli-0.1.10/tests/test_offline.py +0 -34
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/setup.cfg +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/__init__.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/_build_env.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/_version.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/auth/__init__.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/auth/device_flow.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/commands/__init__.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/commands/login.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/commands/workspace.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/platform_client.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/README.md.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/env.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/gitignore.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/pkg/__init__.py.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/pkg/graph.py.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/pyproject.toml.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/hitl → keystone_cli-0.1.26/src/keystone_cli/templates/guardrails}/env.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/hitl → keystone_cli-0.1.26/src/keystone_cli/templates/guardrails}/gitignore.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/hitl → keystone_cli-0.1.26/src/keystone_cli/templates/guardrails}/pkg/__init__.py.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/hitl → keystone_cli-0.1.26/src/keystone_cli/templates/guardrails}/pyproject.toml.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/llm → keystone_cli-0.1.26/src/keystone_cli/templates/hitl}/env.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/llm → keystone_cli-0.1.26/src/keystone_cli/templates/hitl}/gitignore.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/llm → keystone_cli-0.1.26/src/keystone_cli/templates/hitl}/pkg/__init__.py.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/llm → keystone_cli-0.1.26/src/keystone_cli/templates/hitl}/pyproject.toml.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/memory → keystone_cli-0.1.26/src/keystone_cli/templates/kb-builder}/env.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/memory → keystone_cli-0.1.26/src/keystone_cli/templates/kb-builder}/gitignore.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/memory → keystone_cli-0.1.26/src/keystone_cli/templates/kb-builder}/pkg/__init__.py.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/memory → keystone_cli-0.1.26/src/keystone_cli/templates/kb-builder}/pyproject.toml.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/llm/README.md.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/tool-agent → keystone_cli-0.1.26/src/keystone_cli/templates/llm}/env.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/tool-agent → keystone_cli-0.1.26/src/keystone_cli/templates/llm}/gitignore.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/tool-agent → keystone_cli-0.1.26/src/keystone_cli/templates/llm}/pkg/__init__.py.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/rag-qa → keystone_cli-0.1.26/src/keystone_cli/templates/llm}/pyproject.toml.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/rag-qa → keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming}/env.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/rag-qa → keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming}/gitignore.tmpl +0 -0
- {keystone_cli-0.1.10/src/keystone_cli/templates/rag-qa → keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming}/pkg/__init__.py.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory/README.md.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory/pkg/graph.py.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/rag-qa/README.md.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli/templates/tool-agent/README.md.tmpl +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/dependency_links.txt +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/entry_points.txt +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/top_level.txt +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_agent.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_build_progress.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_ci_token.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_invoke.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_login_command.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_platform_client.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_publish.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_retire_delete.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_runs.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_status.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_threads.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_traces.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_version.py +0 -0
- {keystone_cli-0.1.10 → keystone_cli-0.1.26}/tests/test_workspace.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keystone-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.26
|
|
4
4
|
Summary: Keystone pro-code agent CLI — author, validate, and run agents locally, then deploy and invoke them on the Keystone platform.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
|
-
Requires-Dist: keystone-agent-sdk<0.
|
|
6
|
+
Requires-Dist: keystone-agent-sdk<0.11,>=0.10
|
|
7
7
|
Requires-Dist: keystone-request-context<0.4,>=0.3
|
|
8
8
|
Requires-Dist: typer<1,>=0.12
|
|
9
9
|
Requires-Dist: httpx<1,>=0.27
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "keystone-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.26"
|
|
8
8
|
description = "Keystone pro-code agent CLI — author, validate, and run agents locally, then deploy and invoke them on the Keystone platform."
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
dependencies = [
|
|
11
11
|
# Pinned to a compatible range: on an index (Artifact Registry) an unpinned name resolves to
|
|
12
12
|
# ANY published version — the wheel's metadata is the only pin that travels with it.
|
|
13
|
-
"keystone-agent-sdk>=0.
|
|
13
|
+
"keystone-agent-sdk>=0.10,<0.11",
|
|
14
14
|
# Transitive (the SDK's building-block clients) + used directly by `run` to set a local
|
|
15
15
|
# caller-identity ContextVar and to patch the clients for --offline. uv sources are NOT
|
|
16
16
|
# transitive, so declare it directly (cf. services/llm-gateway).
|
|
@@ -45,7 +45,7 @@ where = ["src"]
|
|
|
45
45
|
keystone_cli = ["templates/*/*.tmpl", "templates/*/pkg/*.tmpl"]
|
|
46
46
|
|
|
47
47
|
[tool.ruff]
|
|
48
|
-
target-version = "0.1.
|
|
48
|
+
target-version = "0.1.26"
|
|
49
49
|
line-length = 120
|
|
50
50
|
|
|
51
51
|
[tool.ruff.lint]
|
|
@@ -4,12 +4,20 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
|
+
from keystone_cli._stdio import make_stdio_unicode_safe
|
|
7
8
|
from keystone_cli._version import version_line
|
|
8
9
|
from keystone_cli.commands.agent import agent_app
|
|
9
10
|
from keystone_cli.commands.dev import dev
|
|
11
|
+
from keystone_cli.commands.kb import kb_app
|
|
10
12
|
from keystone_cli.commands.login import login
|
|
13
|
+
from keystone_cli.commands.tool_credentials import tool_credentials_app
|
|
11
14
|
from keystone_cli.commands.workspace import workspace_app
|
|
12
15
|
|
|
16
|
+
# FDP-4360 — before Typer builds anything. `--help` is rendered inside `app()`, and the help
|
|
17
|
+
# text comes from docstrings carrying `→` and `─`, so a hook that ran per-command would be too
|
|
18
|
+
# late for the very invocation QC reported first. No-op on a console that already copes.
|
|
19
|
+
make_stdio_unicode_safe()
|
|
20
|
+
|
|
13
21
|
app = typer.Typer(
|
|
14
22
|
help="Keystone pro-code agent CLI — author, validate, and run agents locally.",
|
|
15
23
|
no_args_is_help=True,
|
|
@@ -45,6 +53,8 @@ def version() -> None:
|
|
|
45
53
|
|
|
46
54
|
app.add_typer(agent_app, name="agent")
|
|
47
55
|
app.add_typer(workspace_app, name="workspace")
|
|
56
|
+
app.add_typer(kb_app, name="kb")
|
|
57
|
+
app.add_typer(tool_credentials_app, name="tool-credentials")
|
|
48
58
|
app.command(name="login")(login)
|
|
49
59
|
app.command(name="dev")(dev)
|
|
50
60
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Make stdout/stderr able to carry the CLI's own glyphs (FDP-4360).
|
|
2
|
+
|
|
3
|
+
Every command prints at least one of ``✓ ✗ → ─ ⚠ ↻``, and Typer renders ``--help`` from
|
|
4
|
+
docstrings that contain them too. On a console whose encoding cannot represent those — a
|
|
5
|
+
Windows cp1252 shell is the reported case, but any ``LANG=C`` box behaves the same — writing
|
|
6
|
+
them raises ``UnicodeEncodeError`` and the command dies at the moment it reports success.
|
|
7
|
+
QC hit exactly that: ``keystone agent init`` created the project and then crashed printing
|
|
8
|
+
``✓``, so the user saw only a traceback and had no way to tell the work had been done.
|
|
9
|
+
|
|
10
|
+
The fix is deliberately conditional rather than unconditional. A stream that can already
|
|
11
|
+
carry the glyphs is left alone — that keeps a deliberate ``PYTHONIOENCODING`` honoured, and
|
|
12
|
+
keeps this from being one more global that reaches into a process it does not own.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from typing import IO, Any
|
|
19
|
+
|
|
20
|
+
#: The characters the CLI actually emits that cp1252 (and most 8-bit encodings) cannot carry.
|
|
21
|
+
#: Taken from the source, not guessed: ``— … • · §`` ARE representable in cp1252 and so would
|
|
22
|
+
#: hide the problem if the probe included only them.
|
|
23
|
+
_PROBE = "→✓✗─⚠↻"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _can_carry_glyphs(stream: IO[Any]) -> bool:
|
|
27
|
+
encoding = getattr(stream, "encoding", None)
|
|
28
|
+
if not encoding:
|
|
29
|
+
# No declared encoding (a pipe wrapper, a test double). Nothing to prove it cannot
|
|
30
|
+
# cope, and nothing to reconfigure either — treat it as fine and leave it be.
|
|
31
|
+
return True
|
|
32
|
+
try:
|
|
33
|
+
_PROBE.encode(encoding)
|
|
34
|
+
except (UnicodeEncodeError, LookupError):
|
|
35
|
+
return False
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_stdio_unicode_safe(streams: tuple[IO[Any], ...] | None = None) -> None:
|
|
40
|
+
"""Re-open stdout/stderr as UTF-8 when their current encoding cannot carry our glyphs.
|
|
41
|
+
|
|
42
|
+
Called once from the CLI entry point, before Typer builds anything — ``--help`` is
|
|
43
|
+
rendered during ``app()``, so a later hook would already be too late.
|
|
44
|
+
|
|
45
|
+
Three things are deliberately tolerated rather than raised on, because a CLI that cannot
|
|
46
|
+
fix its console must still run the command the user asked for:
|
|
47
|
+
|
|
48
|
+
* a stream with no ``reconfigure`` (``io.TextIOWrapper`` has it; pytest's capture object
|
|
49
|
+
and anything hand-rolled may not),
|
|
50
|
+
* ``reconfigure`` refusing — a detached or closed stream,
|
|
51
|
+
* UTF-8 itself being rejected, in which case the encoding is left as it is and only the
|
|
52
|
+
error handler is relaxed, so an unencodable glyph degrades to ``?`` instead of a crash.
|
|
53
|
+
"""
|
|
54
|
+
for stream in streams if streams is not None else (sys.stdout, sys.stderr):
|
|
55
|
+
if _can_carry_glyphs(stream):
|
|
56
|
+
continue
|
|
57
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
58
|
+
if reconfigure is None:
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
62
|
+
except (ValueError, OSError, LookupError):
|
|
63
|
+
# Last resort: keep the console's own encoding and stop it from raising. The
|
|
64
|
+
# glyphs become '?', which is a cosmetic loss; the crash was a functional one.
|
|
65
|
+
try:
|
|
66
|
+
reconfigure(errors="replace")
|
|
67
|
+
except (ValueError, OSError):
|
|
68
|
+
pass
|
|
@@ -95,14 +95,62 @@ def load() -> AuthConfig:
|
|
|
95
95
|
path = config_path()
|
|
96
96
|
if not path.exists():
|
|
97
97
|
return AuthConfig()
|
|
98
|
-
|
|
98
|
+
# FDP-4716: `save()` below writes this file with encoding="utf-8"; reading it back with the
|
|
99
|
+
# locale encoding made the two disagree on Windows, where a workspace name with diacritics
|
|
100
|
+
# turned a valid session into "not logged in".
|
|
101
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
99
102
|
known = {f.name for f in fields(AuthConfig)}
|
|
100
103
|
return AuthConfig(**{k: v for k, v in data.items() if k in known})
|
|
101
104
|
|
|
102
105
|
|
|
106
|
+
def _ensure_private_dir(directory: Path) -> None:
|
|
107
|
+
"""Config dir, owner-only. ``mode=`` is IGNORED when the directory already exists, so the chmod
|
|
108
|
+
is not redundant — it is the only thing that narrows a ``~/.keystone`` an older CLI left at 0755.
|
|
109
|
+
|
|
110
|
+
Best-effort on purpose: on a shared runner the directory can belong to someone else, and failing
|
|
111
|
+
the whole ``login`` over a directory bit would be worse than the bit. The FILE is the credential
|
|
112
|
+
and its mode is enforced unconditionally below; this is the layer around it.
|
|
113
|
+
"""
|
|
114
|
+
directory.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
115
|
+
try:
|
|
116
|
+
os.chmod(directory, 0o700)
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
103
121
|
def save(cfg: AuthConfig) -> None:
|
|
122
|
+
"""Persist the config, with the credential never once readable by anyone else (FDP-4574).
|
|
123
|
+
|
|
124
|
+
This used to be ``write_text()`` then ``chmod(0o600)``. The end state was right, but between the
|
|
125
|
+
two calls the refresh token sat on disk at whatever the umask allowed — 0644 on a default box —
|
|
126
|
+
so on a shared host or CI runner any local user could read it and mint access tokens as the
|
|
127
|
+
developer until it was revoked. Applying permissions AFTER the content is the bug; the window is
|
|
128
|
+
the vulnerability, not the final mode, which is why the existing 0600 assertion never saw it.
|
|
129
|
+
|
|
130
|
+
So the bytes only ever land in a file that was created 0600. ``O_EXCL`` (after clearing a stale
|
|
131
|
+
temp) guarantees WE created it, so ``O_CREAT``'s mode really applies rather than being inherited
|
|
132
|
+
from a leftover file — the one way this shape can silently degrade. umask can only remove bits,
|
|
133
|
+
never add them, so a permissive umask cannot widen it.
|
|
134
|
+
|
|
135
|
+
``os.replace`` then swaps the inode in atomically. Two things fall out for free: a crash can no
|
|
136
|
+
longer leave a half-written config that every later command fails to parse, and an existing
|
|
137
|
+
world-readable ``config.json`` from an older CLI is REPLACED rather than rewritten in place, so
|
|
138
|
+
it is fixed on the next save instead of keeping its old mode forever.
|
|
139
|
+
|
|
140
|
+
The temp carries the pid so two CLI processes saving at once (a `dev` watcher plus a command in
|
|
141
|
+
another terminal) cannot collide on ``O_EXCL``, and it is removed in a ``finally`` so a failed
|
|
142
|
+
save does not leave a token sitting in an orphan file.
|
|
143
|
+
"""
|
|
104
144
|
path = config_path()
|
|
105
|
-
path.parent
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
_ensure_private_dir(path.parent)
|
|
146
|
+
tmp = path.with_name(f"{path.name}.{os.getpid()}.tmp")
|
|
147
|
+
tmp.unlink(missing_ok=True)
|
|
148
|
+
# O_BINARY (Windows only, 0 elsewhere) keeps newline handling in the text wrapper alone, the way
|
|
149
|
+
# open() does it — without it the CRT would translate as well, on top of the wrapper.
|
|
150
|
+
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_BINARY", 0)
|
|
151
|
+
try:
|
|
152
|
+
with os.fdopen(os.open(tmp, flags, 0o600), "w", encoding="utf-8") as fh:
|
|
153
|
+
fh.write(json.dumps(asdict(cfg), indent=2))
|
|
154
|
+
os.replace(tmp, path)
|
|
155
|
+
finally:
|
|
156
|
+
tmp.unlink(missing_ok=True)
|