keystone-cli 0.1.16__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.
Files changed (130) hide show
  1. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/PKG-INFO +2 -2
  2. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/pyproject.toml +3 -3
  3. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/__main__.py +8 -0
  4. keystone_cli-0.1.26/src/keystone_cli/_stdio.py +68 -0
  5. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/commands/agent.py +53 -1
  6. keystone_cli-0.1.26/src/keystone_cli/commands/tool_credentials.py +153 -0
  7. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/runner.py +1 -0
  8. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/scaffold.py +4 -0
  9. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/README.md.tmpl +13 -1
  10. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/pkg/graph.py.tmpl +8 -4
  11. keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming/README.md.tmpl +46 -0
  12. keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming/agent.yaml.tmpl +39 -0
  13. keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming/pkg/graph.py.tmpl +53 -0
  14. keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming/pyproject.toml.tmpl +13 -0
  15. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/rag-qa/agent.yaml.tmpl +5 -0
  16. keystone_cli-0.1.26/src/keystone_cli/templates/tool-agent/env.tmpl +10 -0
  17. keystone_cli-0.1.26/src/keystone_cli/templates/tool-agent/gitignore.tmpl +7 -0
  18. keystone_cli-0.1.26/src/keystone_cli/templates/tool-agent/pkg/__init__.py.tmpl +0 -0
  19. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/tool-agent/pkg/graph.py.tmpl +23 -15
  20. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/tool-agent/pyproject.toml.tmpl +3 -1
  21. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/PKG-INFO +2 -2
  22. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/SOURCES.txt +11 -0
  23. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/requires.txt +1 -1
  24. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_doctor.py +57 -4
  25. keystone_cli-0.1.26/tests/test_encoding.py +131 -0
  26. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_init.py +17 -0
  27. keystone_cli-0.1.26/tests/test_stdio.py +90 -0
  28. keystone_cli-0.1.26/tests/test_tool_credentials.py +94 -0
  29. keystone_cli-0.1.16/tests/test_encoding.py +0 -60
  30. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/setup.cfg +0 -0
  31. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/__init__.py +0 -0
  32. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/_build_env.py +0 -0
  33. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/_version.py +0 -0
  34. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/auth/__init__.py +0 -0
  35. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/auth/device_flow.py +0 -0
  36. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/auth/token_store.py +0 -0
  37. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/commands/__init__.py +0 -0
  38. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/commands/dev.py +0 -0
  39. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/commands/kb.py +0 -0
  40. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/commands/login.py +0 -0
  41. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/commands/workspace.py +0 -0
  42. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/devloop.py +0 -0
  43. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/platform_client.py +0 -0
  44. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/README.md.tmpl +0 -0
  45. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/agent.yaml.tmpl +0 -0
  46. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/env.tmpl +0 -0
  47. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/gitignore.tmpl +0 -0
  48. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/pkg/__init__.py.tmpl +0 -0
  49. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/pkg/graph.py.tmpl +0 -0
  50. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/blank/pyproject.toml.tmpl +0 -0
  51. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/guardrails/README.md.tmpl +0 -0
  52. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/guardrails/agent.yaml.tmpl +0 -0
  53. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/guardrails/env.tmpl +0 -0
  54. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/guardrails/gitignore.tmpl +0 -0
  55. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/guardrails/pkg/__init__.py.tmpl +0 -0
  56. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/guardrails/pkg/graph.py.tmpl +0 -0
  57. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/guardrails/pyproject.toml.tmpl +0 -0
  58. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/agent.yaml.tmpl +0 -0
  59. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/env.tmpl +0 -0
  60. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/gitignore.tmpl +0 -0
  61. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/pkg/__init__.py.tmpl +0 -0
  62. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/hitl/pyproject.toml.tmpl +0 -0
  63. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/kb-builder/README.md.tmpl +0 -0
  64. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/kb-builder/agent.yaml.tmpl +0 -0
  65. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/kb-builder/env.tmpl +0 -0
  66. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/kb-builder/gitignore.tmpl +0 -0
  67. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/kb-builder/pkg/__init__.py.tmpl +0 -0
  68. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/kb-builder/pkg/graph.py.tmpl +0 -0
  69. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/kb-builder/pyproject.toml.tmpl +0 -0
  70. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/llm/README.md.tmpl +0 -0
  71. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/llm/agent.yaml.tmpl +0 -0
  72. {keystone_cli-0.1.16/src/keystone_cli/templates/memory-service → keystone_cli-0.1.26/src/keystone_cli/templates/llm}/env.tmpl +0 -0
  73. {keystone_cli-0.1.16/src/keystone_cli/templates/memory-service → keystone_cli-0.1.26/src/keystone_cli/templates/llm}/gitignore.tmpl +0 -0
  74. {keystone_cli-0.1.16/src/keystone_cli/templates/memory-service → keystone_cli-0.1.26/src/keystone_cli/templates/llm}/pkg/__init__.py.tmpl +0 -0
  75. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/llm/pkg/graph.py.tmpl +0 -0
  76. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/llm/pyproject.toml.tmpl +0 -0
  77. {keystone_cli-0.1.16/src/keystone_cli/templates/llm → keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming}/env.tmpl +0 -0
  78. {keystone_cli-0.1.16/src/keystone_cli/templates/llm → keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming}/gitignore.tmpl +0 -0
  79. {keystone_cli-0.1.16/src/keystone_cli/templates/llm → keystone_cli-0.1.26/src/keystone_cli/templates/llm-streaming}/pkg/__init__.py.tmpl +0 -0
  80. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory/README.md.tmpl +0 -0
  81. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory/agent.yaml.tmpl +0 -0
  82. {keystone_cli-0.1.16/src/keystone_cli/templates/rag-qa → keystone_cli-0.1.26/src/keystone_cli/templates/memory}/env.tmpl +0 -0
  83. {keystone_cli-0.1.16/src/keystone_cli/templates/rag-qa → keystone_cli-0.1.26/src/keystone_cli/templates/memory}/gitignore.tmpl +0 -0
  84. {keystone_cli-0.1.16/src/keystone_cli/templates/rag-qa → keystone_cli-0.1.26/src/keystone_cli/templates/memory}/pkg/__init__.py.tmpl +0 -0
  85. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory/pkg/graph.py.tmpl +0 -0
  86. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory/pyproject.toml.tmpl +0 -0
  87. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory-service/README.md.tmpl +0 -0
  88. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory-service/agent.yaml.tmpl +0 -0
  89. {keystone_cli-0.1.16/src/keystone_cli/templates/memory → keystone_cli-0.1.26/src/keystone_cli/templates/memory-service}/env.tmpl +0 -0
  90. {keystone_cli-0.1.16/src/keystone_cli/templates/memory → keystone_cli-0.1.26/src/keystone_cli/templates/memory-service}/gitignore.tmpl +0 -0
  91. {keystone_cli-0.1.16/src/keystone_cli/templates/memory → keystone_cli-0.1.26/src/keystone_cli/templates/memory-service}/pkg/__init__.py.tmpl +0 -0
  92. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory-service/pkg/graph.py.tmpl +0 -0
  93. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/memory-service/pyproject.toml.tmpl +0 -0
  94. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/rag-qa/README.md.tmpl +0 -0
  95. {keystone_cli-0.1.16/src/keystone_cli/templates/skill → keystone_cli-0.1.26/src/keystone_cli/templates/rag-qa}/env.tmpl +0 -0
  96. {keystone_cli-0.1.16/src/keystone_cli/templates/skill → keystone_cli-0.1.26/src/keystone_cli/templates/rag-qa}/gitignore.tmpl +0 -0
  97. {keystone_cli-0.1.16/src/keystone_cli/templates/skill → keystone_cli-0.1.26/src/keystone_cli/templates/rag-qa}/pkg/__init__.py.tmpl +0 -0
  98. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/rag-qa/pkg/graph.py.tmpl +0 -0
  99. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/rag-qa/pyproject.toml.tmpl +0 -0
  100. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/skill/README.md.tmpl +0 -0
  101. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/skill/agent.yaml.tmpl +0 -0
  102. {keystone_cli-0.1.16/src/keystone_cli/templates/tool-agent → keystone_cli-0.1.26/src/keystone_cli/templates/skill}/env.tmpl +0 -0
  103. {keystone_cli-0.1.16/src/keystone_cli/templates/tool-agent → keystone_cli-0.1.26/src/keystone_cli/templates/skill}/gitignore.tmpl +0 -0
  104. {keystone_cli-0.1.16/src/keystone_cli/templates/tool-agent → keystone_cli-0.1.26/src/keystone_cli/templates/skill}/pkg/__init__.py.tmpl +0 -0
  105. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/skill/pkg/graph.py.tmpl +0 -0
  106. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/skill/pyproject.toml.tmpl +0 -0
  107. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/tool-agent/README.md.tmpl +0 -0
  108. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli/templates/tool-agent/agent.yaml.tmpl +0 -0
  109. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/dependency_links.txt +0 -0
  110. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/entry_points.txt +0 -0
  111. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/src/keystone_cli.egg-info/top_level.txt +0 -0
  112. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_agent.py +0 -0
  113. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_assistant_provenance_and_flags.py +0 -0
  114. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_auth.py +0 -0
  115. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_build_progress.py +0 -0
  116. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_ci_token.py +0 -0
  117. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_deploy.py +0 -0
  118. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_dev.py +0 -0
  119. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_invoke.py +0 -0
  120. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_login_command.py +0 -0
  121. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_offline.py +0 -0
  122. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_platform_client.py +0 -0
  123. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_publish.py +0 -0
  124. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_retire_delete.py +0 -0
  125. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_runs.py +0 -0
  126. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_status.py +0 -0
  127. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_threads.py +0 -0
  128. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_traces.py +0 -0
  129. {keystone_cli-0.1.16 → keystone_cli-0.1.26}/tests/test_version.py +0 -0
  130. {keystone_cli-0.1.16 → 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.16
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,>=0.5
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.16"
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.5,<0.6",
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.16"
48
+ target-version = "0.1.26"
49
49
  line-length = 120
50
50
 
51
51
  [tool.ruff.lint]
@@ -4,13 +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
10
11
  from keystone_cli.commands.kb import kb_app
11
12
  from keystone_cli.commands.login import login
13
+ from keystone_cli.commands.tool_credentials import tool_credentials_app
12
14
  from keystone_cli.commands.workspace import workspace_app
13
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
+
14
21
  app = typer.Typer(
15
22
  help="Keystone pro-code agent CLI — author, validate, and run agents locally.",
16
23
  no_args_is_help=True,
@@ -47,6 +54,7 @@ def version() -> None:
47
54
  app.add_typer(agent_app, name="agent")
48
55
  app.add_typer(workspace_app, name="workspace")
49
56
  app.add_typer(kb_app, name="kb")
57
+ app.add_typer(tool_credentials_app, name="tool-credentials")
50
58
  app.command(name="login")(login)
51
59
  app.command(name="dev")(dev)
52
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
@@ -1486,6 +1486,12 @@ _GRANT_NAMESPACE = {
1486
1486
  "memory": "memory:",
1487
1487
  "skill": "skill:",
1488
1488
  }
1489
+ #: Every name agent-hub acts on (AgentManifestAclGrants): the four above plus rag, whose access is
1490
+ #: the per-KB grant instead of a service-level one. Anything else mints nothing and is not in the
1491
+ #: invocation ticket's scope either, so every call to it fails at runtime — FDP-5461: doctor used to
1492
+ #: skip such a name exactly like rag and still say "wiring looks complete".
1493
+ _KNOWN_SERVICES = frozenset(_GRANT_NAMESPACE) | {"rag"}
1494
+ _KB_QUERY = "knowledge-base:query"
1489
1495
 
1490
1496
 
1491
1497
  @agent_app.command()
@@ -1532,12 +1538,22 @@ def doctor(
1532
1538
  typer.secho(" ? no live version — publish one, then re-run.", fg=typer.colors.YELLOW)
1533
1539
  raise typer.Exit(1)
1534
1540
  declared = set(_doctor_string_list(deps, "services"))
1541
+ kbs = _doctor_string_list(deps, "knowledge_bases")
1535
1542
  agentic_kbs = _doctor_string_list(deps, "agentic_knowledge_bases")
1536
1543
  typer.echo(f" declared: {', '.join(sorted(declared)) or '(none)'}")
1537
1544
 
1545
+ problems += _doctor_check_unknown_services(declared)
1546
+ if "rag" in declared and not kbs and deps.get("manage_knowledge_bases") is not True:
1547
+ # Advice, not a failure: nothing to query yet, but the agent may be given KBs later.
1548
+ typer.secho(
1549
+ " ! rag declared with no knowledge_bases (and no manage_knowledge_bases) — "
1550
+ "every KB call will 403 until you list the KBs it may read.",
1551
+ fg=typer.colors.YELLOW,
1552
+ )
1538
1553
  entries = _doctor_acl_entries(client, base, headers, agent_id)
1539
1554
  if entries is not None:
1540
1555
  problems += _doctor_check_grants(entries, declared)
1556
+ problems += _doctor_check_kbs(entries, kbs)
1541
1557
  problems += _doctor_check_agentic_kbs(entries, agentic_kbs)
1542
1558
  if "memory" in declared:
1543
1559
  problems += _doctor_check_memory(client, base, headers, agent_id, workspace)
@@ -1626,13 +1642,49 @@ def _doctor_check_agentic_kbs(entries: list[dict], declared_agentic: list[str])
1626
1642
  return 1
1627
1643
 
1628
1644
 
1645
+ def _doctor_check_unknown_services(declared: set[str]) -> int:
1646
+ """A declared service agent-hub does not know — almost always a typo (FDP-5461)."""
1647
+ unknown = sorted(declared - _KNOWN_SERVICES)
1648
+ for svc in unknown:
1649
+ typer.secho(
1650
+ f" ✗ {svc:9} is not a building block — nothing is granted for it and every call to it "
1651
+ f"fails. Check the name (known: {', '.join(sorted(_KNOWN_SERVICES))}).",
1652
+ fg=typer.colors.RED,
1653
+ )
1654
+ return len(unknown)
1655
+
1656
+
1657
+ def _doctor_check_kbs(entries: list[dict], declared_kbs: list[str]) -> int:
1658
+ """Did `dependencies.knowledge_bases` actually mint `knowledge-base:query` per KB?
1659
+
1660
+ Counted, not matched by id, for the reason given on the agentic check below: an entry may be a
1661
+ KB NAME that agent-hub resolved at publish. A name it could not resolve is kept as-is and simply
1662
+ mints nothing, so a short count is how a misspelt or foreign KB shows up here.
1663
+ """
1664
+ if not declared_kbs:
1665
+ return 0
1666
+ granted = [r for r in entries if str(r.get("permission", "")) == _KB_QUERY]
1667
+ want = len(declared_kbs)
1668
+ if len(granted) >= want:
1669
+ typer.secho(f" ✓ rag {_KB_QUERY} on {len(granted)} KB(s)", fg=typer.colors.GREEN)
1670
+ return 0
1671
+ typer.secho(
1672
+ f" ✗ rag declares {want} KB(s) but only {len(granted)} {_KB_QUERY} grant(s) landed — "
1673
+ "an entry names a KB that does not exist in this workspace (or is misspelt). Fix "
1674
+ "knowledge_bases and re-publish.",
1675
+ fg=typer.colors.RED,
1676
+ )
1677
+ return 1
1678
+
1679
+
1629
1680
  def _doctor_check_grants(entries: list[dict], declared: set[str]) -> int:
1630
1681
  held = {str(r.get("permission", "")) for r in entries}
1631
1682
  problems = 0
1632
1683
  for svc in sorted(declared):
1633
1684
  prefix = _GRANT_NAMESPACE.get(svc)
1634
1685
  if prefix is None:
1635
- # rag is the deliberate case: its access IS the per-KB grant, not a service-level one.
1686
+ # rag: its access IS the per-KB grant (_doctor_check_kbs). An unknown name was already
1687
+ # reported by _doctor_check_unknown_services — not silently skipped any more.
1636
1688
  continue
1637
1689
  matching = sorted(p for p in held if p.startswith(prefix))
1638
1690
  if matching:
@@ -0,0 +1,153 @@
1
+ """``keystone tool-credentials`` — the secrets a pro-code agent's catalogue tools authenticate with.
2
+
3
+ FDP-3087 P3. A tool credential is created here, then named in ``agent.yaml``::
4
+
5
+ dependencies:
6
+ tool_buckets: [crm_api]
7
+ tool_credentials:
8
+ crm_api: crm-prod-token
9
+
10
+ The value goes to platform once, which writes it to AWS Secrets Manager; nothing ever gives it back
11
+ — not platform, not this CLI. Only the platform's tool gateway reads it, when an agent calls a tool.
12
+
13
+ **The value is never taken from the command line.** An argument lands in shell history and in
14
+ ``ps`` output for every user on the machine; it is read from a hidden prompt, or from stdin with
15
+ ``--from-stdin`` (``pass show crm | keystone tool-credentials create crm-prod-token --from-stdin``).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import sys
21
+
22
+ import httpx
23
+ import typer
24
+
25
+ from keystone_cli.commands.agent import _hub_auth
26
+
27
+ tool_credentials_app = typer.Typer(
28
+ help="Manage the credentials your agents' catalogue tools authenticate with.", no_args_is_help=True
29
+ )
30
+
31
+ _PATH = "/api/platform/v1/tool-credentials"
32
+
33
+
34
+ def _fail(response: httpx.Response, what: str) -> None:
35
+ try:
36
+ body = response.json()
37
+ detail = body.get("detail") or body.get("title") or body
38
+ except ValueError:
39
+ detail = response.text[:300]
40
+ typer.secho(f"✗ {what} failed ({response.status_code}): {detail}", fg=typer.colors.RED)
41
+ raise typer.Exit(1)
42
+
43
+
44
+ def _read_value(from_stdin: bool) -> str:
45
+ if from_stdin:
46
+ # One trailing newline is what `echo` / `pass show` add; anything else is the user's.
47
+ value = sys.stdin.read()
48
+ value = value[:-1] if value.endswith("\n") else value
49
+ value = value[:-1] if value.endswith("\r") else value
50
+ else:
51
+ value = typer.prompt("Value", hide_input=True, confirmation_prompt=True)
52
+ if not value.strip():
53
+ typer.secho("✗ the value is empty", fg=typer.colors.RED)
54
+ raise typer.Exit(1)
55
+ return value
56
+
57
+
58
+ def _resolve_id(client: httpx.Client, base: str, headers: dict[str, str], ref: str) -> str:
59
+ """Accept a name or an id — people remember names, the API speaks ids."""
60
+ response = client.get(f"{base}{_PATH}", headers=headers)
61
+ if response.status_code != httpx.codes.OK:
62
+ _fail(response, "lookup")
63
+ for item in response.json().get("data", []):
64
+ if ref in (item.get("id"), item.get("name")):
65
+ return str(item["id"])
66
+ typer.secho(f"✗ no tool credential named or with id {ref!r} in this workspace", fg=typer.colors.RED)
67
+ raise typer.Exit(1)
68
+
69
+
70
+ @tool_credentials_app.command("list")
71
+ def list_credentials(
72
+ api_base: str | None = typer.Option(None, "--api-base", envvar="KEYSTONE_API_BASE"),
73
+ workspace_id: str | None = typer.Option(None, "--workspace-id"),
74
+ ) -> None:
75
+ """List the tool credentials in this workspace (names and hints — never values)."""
76
+ base, headers = _hub_auth(api_base, workspace_id)
77
+ with httpx.Client(timeout=30) as client:
78
+ response = client.get(f"{base}{_PATH}", headers=headers)
79
+ if response.status_code != httpx.codes.OK:
80
+ _fail(response, "list")
81
+ items = response.json().get("data", [])
82
+ if not items:
83
+ typer.echo("no tool credentials in this workspace")
84
+ return
85
+ typer.echo(f" {'NAME':32} {'ID':38} {'HINT':8} UPDATED")
86
+ for item in items:
87
+ hint = f"…{item['value_hint']}" if item.get("value_hint") else "—"
88
+ typer.echo(
89
+ f" {str(item.get('name', ''))[:32]:32} {item.get('id', ''):38} {hint:8} {item.get('updated_at', '')}"
90
+ )
91
+
92
+
93
+ @tool_credentials_app.command("create")
94
+ def create_credential(
95
+ name: str = typer.Argument(..., help="Unique per workspace — what agent.yaml refers to."),
96
+ description: str = typer.Option("", "--description"),
97
+ from_stdin: bool = typer.Option(False, "--from-stdin", help="Read the value from stdin instead of a prompt."),
98
+ api_base: str | None = typer.Option(None, "--api-base", envvar="KEYSTONE_API_BASE"),
99
+ workspace_id: str | None = typer.Option(None, "--workspace-id"),
100
+ ) -> None:
101
+ """Create a tool credential. For basic auth the value is `user:password`."""
102
+ value = _read_value(from_stdin)
103
+ base, headers = _hub_auth(api_base, workspace_id)
104
+ body: dict[str, str] = {"name": name, "value": value}
105
+ if description:
106
+ body["description"] = description
107
+ with httpx.Client(timeout=30) as client:
108
+ response = client.post(f"{base}{_PATH}", headers=headers, json=body)
109
+ if response.status_code not in (httpx.codes.OK, httpx.codes.CREATED):
110
+ _fail(response, "create")
111
+ data = response.json().get("data", {})
112
+ typer.secho(f"✓ created {data.get('name')} — {data.get('id')}", fg=typer.colors.GREEN)
113
+ typer.echo(f" use it in agent.yaml: dependencies.tool_credentials.<bucket>: {data.get('name')}")
114
+
115
+
116
+ @tool_credentials_app.command("rotate")
117
+ def rotate_credential(
118
+ ref: str = typer.Argument(..., help="Name or id."),
119
+ from_stdin: bool = typer.Option(False, "--from-stdin", help="Read the new value from stdin."),
120
+ api_base: str | None = typer.Option(None, "--api-base", envvar="KEYSTONE_API_BASE"),
121
+ workspace_id: str | None = typer.Option(None, "--workspace-id"),
122
+ ) -> None:
123
+ """Replace the value. Agents keep working and pick the new value up within a minute."""
124
+ value = _read_value(from_stdin)
125
+ base, headers = _hub_auth(api_base, workspace_id)
126
+ with httpx.Client(timeout=30) as client:
127
+ cred_id = _resolve_id(client, base, headers, ref)
128
+ response = client.put(f"{base}{_PATH}/{cred_id}/value", headers=headers, json={"value": value})
129
+ if response.status_code != httpx.codes.OK:
130
+ _fail(response, "rotate")
131
+ typer.secho(f"✓ rotated {ref} — agents use the new value within 60 s", fg=typer.colors.GREEN)
132
+
133
+
134
+ @tool_credentials_app.command("delete")
135
+ def delete_credential(
136
+ ref: str = typer.Argument(..., help="Name or id."),
137
+ force: bool = typer.Option(False, "--force", "--yes", "-y", help="Skip the confirmation."),
138
+ api_base: str | None = typer.Option(None, "--api-base", envvar="KEYSTONE_API_BASE"),
139
+ workspace_id: str | None = typer.Option(None, "--workspace-id"),
140
+ ) -> None:
141
+ """Delete a tool credential. Agents that use it start failing those tool calls within 60 s."""
142
+ if not force:
143
+ typer.confirm(
144
+ f"Delete tool credential {ref}? Agents using it will fail their tool calls within a minute.",
145
+ abort=True,
146
+ )
147
+ base, headers = _hub_auth(api_base, workspace_id)
148
+ with httpx.Client(timeout=30) as client:
149
+ cred_id = _resolve_id(client, base, headers, ref)
150
+ response = client.delete(f"{base}{_PATH}/{cred_id}", headers=headers)
151
+ if response.status_code not in (httpx.codes.OK, httpx.codes.NO_CONTENT):
152
+ _fail(response, "delete")
153
+ typer.secho(f"✓ deleted {ref}", fg=typer.colors.GREEN)
@@ -48,6 +48,7 @@ _OFFLINE_KB: dict[str, Any] = {
48
48
  "top_k": 5,
49
49
  "similarity_threshold": 0.0,
50
50
  "reranker": "none",
51
+ "doc_routing_enabled": False,
51
52
  "chunk_count": 1,
52
53
  "file_count": 1,
53
54
  # The per-KB caps rag returns (schemas/kb.py) — precisely the values an agent would read to
@@ -19,6 +19,10 @@ from keystone.agent_sdk.manifest import validate_agent_name
19
19
  TEMPLATES = (
20
20
  "blank",
21
21
  "llm",
22
+ # FDP-5328 — `llm` with a token-streaming reply: the node calls the model through ChatKeystone,
23
+ # which is what stream_mode=messages republishes. Kept separate rather than changing `llm`, so
24
+ # the plainest template stays one client call a newcomer can read without LangChain.
25
+ "llm-streaming",
22
26
  "rag-qa",
23
27
  "tool-agent",
24
28
  "hitl",
@@ -29,5 +29,17 @@ keystone agent hitl list __AGENT_NAME__
29
29
  keystone agent hitl respond __AGENT_NAME__ <run-id> --approve --note "ok"
30
30
  ```
31
31
 
32
- Use `--reject` to send it down the declined branch instead. See `examples/getting-started.html`
32
+ Use `--reject` to send it down the declined branch instead.
33
+
34
+ ## Before you change `approve`
35
+
36
+ - **The node re-runs on resume.** Everything above `request_approval` in a node runs again when
37
+ the approver answers. Keep it the first statement, and put any paid call before it inside a
38
+ `@durable_task` so the resume replays the result instead of paying twice.
39
+ - **Never catch it.** `request_approval` pauses by raising `GraphInterrupt`, which is an
40
+ `Exception`. A `try/except Exception` around it swallows the pause: the run finishes without
41
+ waiting and the high-impact action goes through unapproved.
42
+ - **Several approvals are fine in sequence** (each one pauses, one at a time). Approvals that
43
+ would pause **in parallel** — parallel branches or `Send` fan-out — are not supported: only the
44
+ first reaches an approver. See `examples/getting-started.html`
33
45
  for the full flow and the `demo-helpdesk` / `support-triage` examples for richer HITL graphs.
@@ -9,10 +9,14 @@
9
9
  ``request_approval`` — the run parks as ``awaiting_human`` until a person answers with
10
10
  ``keystone agent hitl respond``. Routine requests skip straight to release.
11
11
 
12
- Two rules the platform enforces for HITL (see keystone.agent_sdk.request_approval):
13
- * Call ``request_approval`` EARLY in the node — on resume the whole node re-runs up to it,
14
- so nothing paid/side-effecting may run before it. Here it is the first statement.
15
- * Paid work goes through ``@durable_task`` so a crash-resume replays it, never re-bills it.
12
+ Three HITL rules (full text in keystone.agent_sdk.request_approval):
13
+ * The node body RE-RUNS from the top on every resume, so anything before
14
+ ``request_approval`` runs again. Call it EARLY — here it is the first statement.
15
+ * Paid work goes through ``@durable_task``: on a resume (or a crash-resume) it replays its
16
+ stored result instead of calling the model again, so it is billed once.
17
+ * NEVER wrap ``request_approval`` in ``try/except Exception``. It pauses by raising
18
+ ``GraphInterrupt``; a broad except swallows it, the run never waits for the approver, and
19
+ the action that needed approval happens anyway.
16
20
 
17
21
  Offline the model stub isn't valid JSON, so ``assess`` defaults to routine and the run
18
22
  completes without ever pausing (a local run has no checkpointer to park on).
@@ -0,0 +1,46 @@
1
+ # __AGENT_NAME__
2
+
3
+ Scaffolded from the **llm-streaming** template — the `llm` agent, but the reply streams **token by
4
+ token**. One node calls the model through **llm-gateway** via `ChatKeystone`.
5
+
6
+ ```
7
+ __PKG__/graph.py # respond: ChatKeystone(client=get_gateway_client()).ainvoke(...) → reply
8
+ agent.yaml # declares llm-gateway + the model; io_schema = { prompt }
9
+ ```
10
+
11
+ ## Why a separate template
12
+
13
+ `POST …/invoke/stream?stream_mode=messages` republishes the chunks a LangChain **chat model**
14
+ produces while a node runs. The `llm` template calls `GatewayClient.chat()` — plain HTTP — so its
15
+ stream carries state updates and **no tokens**. `ChatKeystone` wraps the same pooled client, so
16
+ grants, budget, identity and tracing behave exactly as in `llm`.
17
+
18
+ ## Run it locally
19
+
20
+ ```bash
21
+ keystone agent validate .
22
+ keystone agent run . --input '{"prompt": "explain retries in one sentence"}' --offline
23
+ ```
24
+
25
+ `--offline` mocks the gateway call and does not stream; it proves the wiring only.
26
+
27
+ ## Deploy, then watch it stream
28
+
29
+ ```bash
30
+ keystone agent deploy . --publish
31
+ ```
32
+
33
+ The CLI has no streaming subcommand yet, so stream with `curl` (`-N` turns off buffering — without
34
+ it the tokens arrive all at once and look like no streaming at all):
35
+
36
+ ```bash
37
+ curl -N -X POST "$AGENT_API/api/agent-service/v1/agents/__AGENT_NAME__/invoke/stream?stream_mode=messages" \
38
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
39
+ -d '{"inputs": {"prompt": "explain retries in one sentence"}}'
40
+ ```
41
+
42
+ Each `event: token` frame is one chunk. If you only see `state` frames and one final answer, the
43
+ node is not calling a chat model — check it still goes through `ChatKeystone`.
44
+
45
+ The model in `dependencies.models` must be granted to your workspace, or deploy fails preflight
46
+ with `422 AGENT_DEPENDENCY_NOT_GRANTED`.
@@ -0,0 +1,39 @@
1
+ # bundle-rev: 0001 — bump this string to force a rebuild (agent-hub dedups identical bundles by SHA-256)
2
+ # __AGENT_NAME__ — scaffolded by `keystone agent init` (template: llm-streaming).
3
+ # The llm template, but the reply streams token by token: the node calls the model through
4
+ # ChatKeystone (a LangChain chat model over the gateway client), which is what LangGraph's
5
+ # stream_mode=messages republishes. Same inputs, same output, same grants as `llm`.
6
+ # Local loop:
7
+ # keystone agent validate .
8
+ # keystone agent run . --input '{"prompt": "explain retries in one sentence"}' --offline
9
+ # Deploy (server-side build — no Dockerfile needed):
10
+ # keystone agent deploy . --publish
11
+ name: __AGENT_NAME__
12
+ workspace: __WORKSPACE__
13
+ framework: langgraph@1
14
+ entrypoint: __PKG__.graph:make_graph
15
+ runtime: python3.12
16
+ resources:
17
+ timeout_seconds: 60
18
+ # Enforced at invoke (FDP-4694): a payload that breaks this is refused with 422 BEFORE a run
19
+ # starts, naming the field. Extra keys are allowed — add `additionalProperties: false` if you
20
+ # want them rejected too. Declaring nothing here means every input is accepted.
21
+ io_schema:
22
+ type: object
23
+ properties:
24
+ prompt:
25
+ type: string
26
+ required: [prompt]
27
+ dependencies:
28
+ services: [llm-gateway]
29
+ models: [bedrock/anthropic.claude-sonnet-4-6]
30
+ # Guardrails policy, applied by `await guard_input(messages=turn)` with no config= (FDP-4567).
31
+ # Declare `guardrails` under dependencies.services too, or the grant is never minted.
32
+ # guardrails:
33
+ # input: {pii: {enabled: true, on_detect: mask}}
34
+ # output: {secrets: {enabled: true, on_detect: block}}
35
+ identity:
36
+ scopes: [gateway:chat]
37
+ # Per-agent config — injected into the pod as env by the operator.
38
+ config:
39
+ RT_AGENT_MODEL: "bedrock/anthropic.claude-sonnet-4-6"
@@ -0,0 +1,53 @@
1
+ """__AGENT_NAME__ — an llm-gateway agent whose reply STREAMS token by token.
2
+
3
+ Same shape as the ``llm`` template, one difference that matters: the model is called through
4
+ ``ChatKeystone`` instead of ``GatewayClient.chat()``.
5
+
6
+ Why that is the whole trick: ``POST …/invoke/stream?stream_mode=messages`` republishes the chunks a
7
+ LangChain *chat model* produces while a node runs. A node that calls raw HTTP produces none, so the
8
+ stream carries state updates and no tokens — however fast the model answers. ``ChatKeystone`` wraps
9
+ the SAME pooled gateway client, so model grants, budget, caller identity and tracing are unchanged.
10
+
11
+ Nothing in this file streams by itself: ``ainvoke`` returns the full reply to the graph, and the
12
+ runtime taps the tokens on the way. ``invoke`` (non-streaming) keeps working exactly like ``llm``.
13
+
14
+ Local loop:
15
+ keystone agent run . --input '{"prompt": "explain retries in one sentence"}' --offline
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from typing import TypedDict
22
+
23
+ from keystone.agent_sdk import ChatKeystone
24
+ from keystone.agent_sdk.clients import get_gateway_client
25
+ from langchain_core.messages import HumanMessage, SystemMessage
26
+ from langgraph.graph import StateGraph
27
+
28
+ _SYSTEM = "You are a concise, helpful assistant. Answer in a few sentences."
29
+
30
+
31
+ class State(TypedDict, total=False):
32
+ prompt: str
33
+ reply: str
34
+
35
+
36
+ async def respond(state: State) -> dict[str, str]:
37
+ # Hand the model the pooled per-run client (get_gateway_client) rather than letting it build
38
+ # one: a client constructed per call opens a connection pool that is never closed (FDP-4567).
39
+ model = ChatKeystone(
40
+ client=get_gateway_client(),
41
+ model=os.environ.get("RT_AGENT_MODEL", "bedrock/anthropic.claude-sonnet-4-6"),
42
+ )
43
+ answer = await model.ainvoke([SystemMessage(_SYSTEM), HumanMessage(state.get("prompt", ""))])
44
+ # `.content`, not `.text`: a property in langchain-core 1.x, a deprecated method before it.
45
+ return {"reply": answer.content}
46
+
47
+
48
+ def make_graph():
49
+ g = StateGraph(State)
50
+ g.add_node("respond", respond)
51
+ g.set_entry_point("respond")
52
+ g.set_finish_point("respond")
53
+ return g
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "__AGENT_NAME__"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ # Declared for humans/IDEs: the DEPLOYED image is built FROM the platform base image,
6
+ # which already bakes keystone-agent-sdk + langgraph — the server-side (Kaniko) build
7
+ # copies your code only, it does NOT pip-install. Locally, `keystone agent validate/run`
8
+ # execute in the CLI's own environment.
9
+ dependencies = [
10
+ # >=0.6: ChatKeystone landed there; a looser floor resolves an SDK without it and fails at import.
11
+ "keystone-agent-sdk>=0.6",
12
+ "langgraph>=1,<2",
13
+ ]
@@ -52,5 +52,10 @@ config:
52
52
  # Placeholder, not empty: `keystone agent run . --offline` then works out of the box (the rag
53
53
  # call is stubbed), while a deploy that forgets to fill it fails with THIS string in the error
54
54
  # instead of a shape complaint about your query.
55
+ #
56
+ # This id lives in TWO places and they must agree: the line above mints the grant, this one is
57
+ # what the graph queries with. `keystone agent validate .` now says so when they drift — which
58
+ # matters most in the direction that is otherwise silent: a KB granted but never queried leaves
59
+ # the agent answering from no retrieved context, with no error and no 403 (FDP-5164).
55
60
  RT_AGENT_KB_ID: "REPLACE_WITH_YOUR_KB_ID" # ← same id as knowledge_bases above
56
61
  RT_AGENT_MODEL: "bedrock/anthropic.claude-sonnet-4-6"
@@ -0,0 +1,10 @@
1
+ # Local-only environment for `__AGENT_NAME__`. NEVER committed (.gitignore) and NEVER packaged —
2
+ # `keystone agent deploy` drops .env* unconditionally, because a bundle goes to S3 and into the
3
+ # agent's image in ECR, where a copy would outlive a revoked key.
4
+ #
5
+ # On the cluster the operator injects these, so leave them empty here unless you run locally.
6
+
7
+ # The agent's platform API key. Create it on the portal (Access policy → API keys) and paste the
8
+ # value shown ONCE. Read automatically: agent-runtime settings use env_prefix="RT_" + env_file=".env",
9
+ # so the RT_ prefix is required — a differently-named variable is IGNORED SILENTLY.
10
+ RT_AGENT_API_KEY=
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .pytest_cache/
5
+ # Secrets + local config. Also excluded from the deploy bundle unconditionally.
6
+ .env
7
+ .env.*