kairos-chain 3.88.0 → 3.88.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/agent/lib/agent/admission.rb +200 -0
- data/templates/skillsets/agent/lib/agent/cognitive_loop.rb +9 -0
- data/templates/skillsets/agent/plugin/SKILL.md +9 -2
- data/templates/skillsets/agent/test/test_agent_act_hardening.rb +488 -0
- data/templates/skillsets/agent/test/test_agent_guard_verdict_record.rb +448 -0
- data/templates/skillsets/agent/tools/agent_step.rb +390 -35
- data/templates/skillsets/document_authoring/lib/document_authoring/section_writer.rb +6 -1
- data/templates/skillsets/llm_client/lib/llm_client/cursor_adapter.rb +23 -8
- data/templates/skillsets/llm_client/test/test_cursor_adapter_model.rb +4 -4
- data/templates/skillsets/llm_client/test/test_cursor_adapter_readonly.rb +47 -0
- data/templates/skillsets/minimum_nomic/bin/run_gm.rb +104 -2
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a785289fdb24dc921cad6c727b74168557a7dcf90593cb7ae267d2cb6b3501f
|
|
4
|
+
data.tar.gz: 422bb014ce8b479f765972b4f05fe57f86c71c09bae7c48277e8625b798767af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b399c8a6e440d323ce30c73fb1c62acbb6fcefe7c67e76b89bed4171a3a9bc57d3af48de2ffb4cfc9ac4a802498dde65a868e0435564824d1f29a638748f3e73
|
|
7
|
+
data.tar.gz: 6b2fc4894c4d52489bb86580386a50121c64ebcdc7f1e363e18813f0a9a548d2a9f974104206fc66d4255b00119cb6f9d4c4b3b81523776a365c4b37be58779b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,108 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [3.88.2] - 2026-10-06
|
|
8
|
+
|
|
9
|
+
### Security — agent: the act and its LLM bodies stay inside their gates
|
|
10
|
+
|
|
11
|
+
A design review of approval delegation found several ways for the agent to get
|
|
12
|
+
around the gates it runs inside: the tool blacklist, the risk budget and the
|
|
13
|
+
guard. Each was confirmed in code; none was observed in use. Four review rounds
|
|
14
|
+
followed. **The act route is still deny-based. Do not run the agent unattended
|
|
15
|
+
until an act-route allow-list lands** (next version, designed with approval
|
|
16
|
+
delegation).
|
|
17
|
+
|
|
18
|
+
**No LLM body launched for the agent carries tools in the project root.**
|
|
19
|
+
- **Agent phases.** ORIENT, DECIDE, REFLECT and the persona reviews launched
|
|
20
|
+
`claude -p` in the project root. It read the project's instruction files and
|
|
21
|
+
inherited its permission rules (on one instance, `Bash(*)`, `Write` and
|
|
22
|
+
`Edit`). Every agent LLM call is now sandboxed.
|
|
23
|
+
- **llm_call steps in the act.** The driver forces `sandbox_mode` on them. A
|
|
24
|
+
step whose provider carries tools (codex, cursor, codex_mcp), or any provider
|
|
25
|
+
outside claude_code / anthropic / openai / bedrock, is refused before the
|
|
26
|
+
act. This includes the instance's default provider when the step names
|
|
27
|
+
none, and the default provider used by `write_section`.
|
|
28
|
+
- **`write_section`.** Its section writer now always calls the LLM sandboxed.
|
|
29
|
+
- **multi_llm_review cursor seat.** It ran `agent -p` (every tool, write and
|
|
30
|
+
shell included) in the server's working directory. It now runs
|
|
31
|
+
`agent -p --trust --mode plan` (read-only) in an empty directory made for
|
|
32
|
+
each call. Cursor reviews therefore see only the artifact.
|
|
33
|
+
|
|
34
|
+
**An act does not touch what constrains the agent or its operator.**
|
|
35
|
+
- **Protected locations.** A step whose path arguments, at any depth and
|
|
36
|
+
whatever the tool, reach a protected location is refused before ACT. The
|
|
37
|
+
protected locations are:
|
|
38
|
+
- the KairosChain stores (`.kairos`: config, sessions, L1, the instruction
|
|
39
|
+
mode, the chain, the keys);
|
|
40
|
+
- `.claude` and `.codex`;
|
|
41
|
+
- `.mcp.json`, `CLAUDE.md` and `AGENTS.md`;
|
|
42
|
+
- anything outside the project (`$HOME` and `/` never count as the
|
|
43
|
+
project).
|
|
44
|
+
- **How paths are judged.** Paths are resolved against the step's own root
|
|
45
|
+
arguments and against the tools' default roots, so a decoy root cannot move
|
|
46
|
+
the check. `..` segments, unresolvable paths and dangling symlinks are
|
|
47
|
+
refused. A symlink is judged by its target.
|
|
48
|
+
- **Recording.** The refusal is recorded on the chain (`agent_act_refused`,
|
|
49
|
+
with the rule that fired) whether or not the guard is on.
|
|
50
|
+
- **Record-store writers** (`chain_record`, attestation and state commits) are
|
|
51
|
+
now refused on the act route with the guard off as well. The record judges
|
|
52
|
+
the act, so the act may not write it.
|
|
53
|
+
- **Configuration writers** (`llm_configure`, `mode_hooks_add` /
|
|
54
|
+
`mode_hooks_project`, `plugin_project`), `hermes_*` and `multi_llm_review*`
|
|
55
|
+
are refused on the act route. All of these are also hidden from DECIDE's
|
|
56
|
+
tool catalog, and DECIDE's norms name the rule.
|
|
57
|
+
- **Instances whose `safety.yml` sets no `safe_root`.** The file tools resolve
|
|
58
|
+
relative paths inside the stores there, so the act refuses them. The refusal
|
|
59
|
+
says to set `safe_root`.
|
|
60
|
+
|
|
61
|
+
**Not covered yet.**
|
|
62
|
+
- `.git/hooks` and `.git/config`, `.cursor*`, `.gemini`,
|
|
63
|
+
`.github/copilot-instructions.md`, `.envrc` and `.vscode/tasks.json` are not
|
|
64
|
+
in the protected set.
|
|
65
|
+
- A path-less tool not named above is not stopped (the allow-list will).
|
|
66
|
+
- `agent_execute` remains unregistered.
|
|
67
|
+
- In an operator's own multi_llm_review runs, the codex seat can still read
|
|
68
|
+
the repository.
|
|
69
|
+
|
|
70
|
+
## [3.88.1] - 2026-10-06
|
|
71
|
+
|
|
72
|
+
### Fixed — agent guard: the verdict reaches the record
|
|
73
|
+
|
|
74
|
+
Found in a guarded trial on 3.88.0. The guard judged correctly, but its
|
|
75
|
+
judgment never reached the record: the chain held only autoexec's "execution
|
|
76
|
+
complete" for an act the guard had failed, progress said `completed`, and the
|
|
77
|
+
FAIL survived only in a mutable session file.
|
|
78
|
+
|
|
79
|
+
- **The verdict is chain-recorded.** Right after judging, before any halt
|
|
80
|
+
checkpoint and before any merge, the driver records `kind:
|
|
81
|
+
agent_guard_verdict`: the verdict, the pinned spec hash, the failed checks,
|
|
82
|
+
the planned route, and the act's error if it had one.
|
|
83
|
+
- **A verdict the chain does not take stops the loop.** Whatever the verdict,
|
|
84
|
+
nothing merges and the cycle halts for the operator. The halt reason names
|
|
85
|
+
the lost verdict and its reason, why the record failed, and whether the act
|
|
86
|
+
already took effect (in-process) or stays quarantined (confined). Responses
|
|
87
|
+
and the progress `guard_record` carry `verdict_recorded: false` and
|
|
88
|
+
`lost_verdict`. On a backend whose chain cannot be appended to (sqlite and
|
|
89
|
+
postgresql do not declare a ledger path), every guarded cycle now halts
|
|
90
|
+
this way instead of running unrecorded.
|
|
91
|
+
- **Progress and responses follow the verdict.** A failed verdict is recorded
|
|
92
|
+
and reported as `failed`, not `completed`; REFLECT is shown the verdict, and
|
|
93
|
+
the next cycle's executor context says the cycle failed. Manual responses
|
|
94
|
+
carry `guard` (verdict, failed checks) and `act_error`. The manual risk-resume
|
|
95
|
+
path reports a guard halt as `guard_halt`, not `ok`. Autonomous responses
|
|
96
|
+
carry the halt reason and, per cycle, `guard_verdict`, `guard_reason` and
|
|
97
|
+
`verdict_recorded`; steps set aside for a person are still listed when a
|
|
98
|
+
cycle halts. A result carrying only an error is no longer reported as
|
|
99
|
+
`completed`.
|
|
100
|
+
|
|
101
|
+
Not fixed here: the confined route still cannot run. `agent_execute` is not
|
|
102
|
+
registered by the agent SkillSet, so a plan naming a file tool fails at ACT
|
|
103
|
+
(with the guard on, that FAIL is now recorded), and a file acceptance check
|
|
104
|
+
cannot PASS in this version. Wiring it is separate work.
|
|
105
|
+
|
|
106
|
+
Also ships `minimum_nomic`'s `bin/run_gm.rb` instance-local changes (a11d103),
|
|
107
|
+
so `system_upgrade` stops reverting them.
|
|
108
|
+
|
|
7
109
|
## [3.88.0] - 2026-09-30
|
|
8
110
|
|
|
9
111
|
### Added — `multi_llm_review` 0.11.1: an artifact named by path and sha256
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -54,8 +54,208 @@ module KairosMcp
|
|
|
54
54
|
# route, where the scratch area quarantines it until the verdict.
|
|
55
55
|
LIVE_TREE_WRITE_TOOLS = %w[write_section sc_scaffold plugin_project].freeze
|
|
56
56
|
|
|
57
|
+
# What constrains the agent or its operator, and so may not be touched
|
|
58
|
+
# by an act (read or written) through any path argument of any tool:
|
|
59
|
+
# the KairosChain stores (the agent's own config and session files,
|
|
60
|
+
# L1, the instruction mode, the chain), the harness configuration
|
|
61
|
+
# (.claude: permission rules and hooks that run as the operator;
|
|
62
|
+
# .codex; .mcp.json) and the instruction files every agent body reads
|
|
63
|
+
# (CLAUDE.md, AGENTS.md). Checked by destination, not by tool name, so
|
|
64
|
+
# a tool that was not anticipated is covered by its arguments.
|
|
65
|
+
PROTECTED_SEGMENTS = %w[.kairos .claude .codex].freeze
|
|
66
|
+
PROTECTED_BASENAMES = %w[claude.md agents.md .mcp.json].freeze
|
|
67
|
+
|
|
68
|
+
# Argument names that carry a filesystem location, matched by word
|
|
69
|
+
# (split on '_' / '-') so 'profile' or 'output_format' is not a path.
|
|
70
|
+
# Tokens come from splitting on '_', '-' and camelCase. A token also
|
|
71
|
+
# matches when it ends in 'dir' or 'path' (workdir, outdir, filepath)
|
|
72
|
+
# — not 'file', or 'profile' would match.
|
|
73
|
+
PATH_TOKENS = %w[path paths file files filename filenames dir dirs dirname directory dest
|
|
74
|
+
destination source target root workspace src dst cwd folder location].freeze
|
|
75
|
+
PATH_SUFFIXES = %w[dir path].freeze
|
|
76
|
+
ROOT_TOKENS = %w[root workspace cwd].freeze
|
|
77
|
+
|
|
78
|
+
# Tools an act may not call, with or without the guard: they rewrite
|
|
79
|
+
# the configuration that governs every later call (llm_configure:
|
|
80
|
+
# provider, endpoint and which key is sent; the mode-hook tools and
|
|
81
|
+
# plugin_project: .claude hooks and permission surfaces) without
|
|
82
|
+
# naming a path, or launch an external agent body (hermes_*).
|
|
83
|
+
# multi_llm_review is here too: its codex seat runs `codex exec` (read-
|
|
84
|
+
# only, but able to read files) in the project root, and its cursor
|
|
85
|
+
# seat may read absolute paths, so a plan-authored artifact could have
|
|
86
|
+
# a seat read the instance's keys. The driver's own review gate is not
|
|
87
|
+
# the act route and is unaffected.
|
|
88
|
+
ACT_CONFIG_WRITERS = %w[llm_configure mode_hooks_add mode_hooks_project plugin_project hermes_*
|
|
89
|
+
multi_llm_review*].freeze
|
|
90
|
+
|
|
91
|
+
# llm_call providers an act may use: claude_code runs sandboxed (the
|
|
92
|
+
# driver forces sandbox_mode); the API providers carry no tools. CLI
|
|
93
|
+
# bodies that carry tools (codex, codex_mcp, cursor) and any provider
|
|
94
|
+
# not named here are refused.
|
|
95
|
+
# (openrouter is not listed: without a configured base_url it falls
|
|
96
|
+
# back to the OpenAI endpoint and would send the OpenRouter key there.)
|
|
97
|
+
ACT_SAFE_LLM_PROVIDERS = %w[claude_code anthropic openai bedrock].freeze
|
|
98
|
+
|
|
99
|
+
# Tools that call llm_call internally with the instance's default
|
|
100
|
+
# provider (no override possible from the plan).
|
|
101
|
+
DEFAULT_PROVIDER_LLM_TOOLS = %w[write_section].freeze
|
|
102
|
+
|
|
57
103
|
module_function
|
|
58
104
|
|
|
105
|
+
# Steps of a plan that would touch a protected location or leave the
|
|
106
|
+
# project, as [{ 'step_id', 'tool_name', 'path' }].
|
|
107
|
+
# protected_dirs absolute protected directories (the stores,
|
|
108
|
+
# <project>/.claude, <project>/.codex)
|
|
109
|
+
# roots every directory a tool may resolve a relative path
|
|
110
|
+
# against
|
|
111
|
+
# allowed_roots where an act may touch anything at all (the
|
|
112
|
+
# project); a path or a root argument resolving
|
|
113
|
+
# outside them is refused, so an explicit
|
|
114
|
+
# workspace_root cannot turn the check into a
|
|
115
|
+
# deny-list over the whole disk
|
|
116
|
+
# Every argument at any depth whose name is path-like is checked.
|
|
117
|
+
# Fail-closed throughout: arguments that are not an object, a '..'
|
|
118
|
+
# segment, an unresolvable path and a dangling symlink are hits.
|
|
119
|
+
# Comparison ignores case (APFS is case-insensitive by default).
|
|
120
|
+
def protected_path_violations(task_json, protected_dirs, roots, allowed_roots = nil)
|
|
121
|
+
protected = Array(protected_dirs).map { |d| safe_canonical(d).downcase }
|
|
122
|
+
allowed = Array(allowed_roots).compact.map { |d| safe_canonical(d) }
|
|
123
|
+
steps = task_json.is_a?(Hash) ? task_json['steps'] : nil
|
|
124
|
+
return [] unless steps.is_a?(Array)
|
|
125
|
+
|
|
126
|
+
steps.flat_map do |step|
|
|
127
|
+
next [{ 'step_id' => nil, 'tool_name' => nil, 'path' => '(malformed step)' }] unless step.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
args = step['tool_arguments']
|
|
130
|
+
next [] if args.nil?
|
|
131
|
+
unless args.is_a?(Hash)
|
|
132
|
+
next [{ 'step_id' => step['step_id'], 'tool_name' => step['tool_name'], 'path' => '(arguments not an object)' }]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
pairs = path_pairs(args)
|
|
136
|
+
explicit_roots = pairs.select { |k, _| (key_tokens(k) & ROOT_TOKENS).any? }.map(&:last)
|
|
137
|
+
# Resolve against the explicit roots AND the tools' default roots: a
|
|
138
|
+
# root argument in the plan is text the planner writes, while the
|
|
139
|
+
# tool decides where it actually resolves (some tools ignore the
|
|
140
|
+
# argument). Judging only at the plan's root would let a decoy
|
|
141
|
+
# 'workspace_root' move the check away from the real write. On an
|
|
142
|
+
# instance whose default root is the stores this over-refuses; the
|
|
143
|
+
# refusal tells the operator to set safe_root.
|
|
144
|
+
candidate_roots = (explicit_roots + Array(roots)).compact.map(&:to_s).reject(&:empty?)
|
|
145
|
+
# No root to resolve against means the check cannot be made.
|
|
146
|
+
next [{ 'step_id' => step['step_id'], 'tool_name' => step['tool_name'], 'path' => '(no root)',
|
|
147
|
+
'rule' => 'unresolvable' }] if candidate_roots.empty? && !pairs.empty?
|
|
148
|
+
pairs.filter_map do |_key, raw|
|
|
149
|
+
rule = if protected_name?(raw) then 'protected'
|
|
150
|
+
elsif raw.split(%r{[/\\]}).include?('..') then 'dotdot'
|
|
151
|
+
else candidate_roots.lazy.map { |root| touch_rule(raw, root, protected, allowed) }.find(&:itself)
|
|
152
|
+
end
|
|
153
|
+
{ 'step_id' => step['step_id'], 'tool_name' => step['tool_name'], 'path' => raw, 'rule' => rule } if rule
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def key_tokens(key)
|
|
159
|
+
key.to_s.gsub(/([a-z0-9])([A-Z])/, '\\1_\\2').downcase.split(/[_\-]/)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def path_key?(key)
|
|
163
|
+
key_tokens(key).any? do |t|
|
|
164
|
+
PATH_TOKENS.include?(t) || PATH_SUFFIXES.any? { |suf| t.end_with?(suf) }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# [key, string] for every path-like key at any depth; an array takes
|
|
169
|
+
# its key from the parent.
|
|
170
|
+
def path_pairs(node, key = nil)
|
|
171
|
+
case node
|
|
172
|
+
when Hash then node.flat_map { |k, v| path_pairs(v, k) }
|
|
173
|
+
when Array then node.flat_map { |v| path_pairs(v, key) }
|
|
174
|
+
when String
|
|
175
|
+
key && path_key?(key) && !node.strip.empty? ? [[key.to_s, node]] : []
|
|
176
|
+
else []
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# llm_call steps whose provider would launch a tool-carrying body:
|
|
181
|
+
# [{ 'step_id', 'tool_name', 'path' => '(provider X)', 'rule' }].
|
|
182
|
+
# default_provider is the instance's configured provider (nil when it
|
|
183
|
+
# cannot be read, which refuses steps that do not name a safe one).
|
|
184
|
+
def unsafe_llm_steps(task_json, default_provider)
|
|
185
|
+
steps = task_json.is_a?(Hash) && task_json['steps'].is_a?(Array) ? task_json['steps'] : []
|
|
186
|
+
steps.filter_map do |step|
|
|
187
|
+
next unless step.is_a?(Hash)
|
|
188
|
+
|
|
189
|
+
tool = step['tool_name'].to_s
|
|
190
|
+
next unless tool == 'llm_call' || DEFAULT_PROVIDER_LLM_TOOLS.include?(tool)
|
|
191
|
+
|
|
192
|
+
args = step['tool_arguments'].is_a?(Hash) ? step['tool_arguments'] : {}
|
|
193
|
+
provider = tool == 'llm_call' ? args['provider_override'].to_s : ''
|
|
194
|
+
provider = default_provider.to_s if provider.empty?
|
|
195
|
+
next if ACT_SAFE_LLM_PROVIDERS.include?(provider)
|
|
196
|
+
|
|
197
|
+
{ 'step_id' => step['step_id'], 'tool_name' => tool,
|
|
198
|
+
'path' => "(provider #{provider.empty? ? 'unknown' : provider})", 'rule' => 'unsafe_provider' }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# A protected segment or file name anywhere in the path refuses it,
|
|
203
|
+
# whatever root the tool resolves against.
|
|
204
|
+
def protected_name?(raw)
|
|
205
|
+
parts = raw.split(%r{[/\\]}).map(&:downcase)
|
|
206
|
+
parts.any? { |p| PROTECTED_SEGMENTS.include?(p) } || PROTECTED_BASENAMES.include?(parts.last)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# True when the path, resolved against root, lands in a protected
|
|
210
|
+
# directory, lies outside every allowed root, or has a protected name
|
|
211
|
+
# once symlinks are resolved. Names are checked only below the allowed
|
|
212
|
+
# root, so a project that itself lives under a protected segment (a
|
|
213
|
+
# Claude Code worktree under .claude/worktrees) is not refused wholesale.
|
|
214
|
+
# The rule a path breaks when resolved against root, or nil.
|
|
215
|
+
# Protected directories compare case-insensitively (fail-closed on
|
|
216
|
+
# case-insensitive filesystems); containment compares with case kept,
|
|
217
|
+
# so on a case-sensitive filesystem /srv/project is not inside
|
|
218
|
+
# /srv/Project. No allowed root at all means nothing is allowed.
|
|
219
|
+
def touch_rule(path, root, protected, allowed)
|
|
220
|
+
full = canonical(File.expand_path(path, root))
|
|
221
|
+
low = full.downcase
|
|
222
|
+
return 'protected' if protected.any? { |dir| low == dir || low.start_with?("#{dir}/") }
|
|
223
|
+
return 'outside' if allowed.empty?
|
|
224
|
+
|
|
225
|
+
base = allowed.select { |dir| full == dir || full.start_with?("#{dir}/") }.max_by(&:length)
|
|
226
|
+
return 'outside' unless base
|
|
227
|
+
|
|
228
|
+
below = full.delete_prefix(base).delete_prefix('/')
|
|
229
|
+
!below.empty? && protected_name?(below) ? 'protected' : nil
|
|
230
|
+
rescue StandardError
|
|
231
|
+
'unresolvable'
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def touches?(path, root, protected, allowed)
|
|
235
|
+
!touch_rule(path, root, protected, allowed).nil?
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# realpath of the deepest existing ancestor, with the rest appended,
|
|
239
|
+
# so a symlink inside the workspace that points at a protected
|
|
240
|
+
# location is seen. A symlink counts as existing even when its target
|
|
241
|
+
# does not, and its realpath then raises: a dangling link is a hit.
|
|
242
|
+
def canonical(path)
|
|
243
|
+
existing = path
|
|
244
|
+
rest = []
|
|
245
|
+
until File.exist?(existing) || File.symlink?(existing) || existing == File.dirname(existing)
|
|
246
|
+
rest.unshift(File.basename(existing))
|
|
247
|
+
existing = File.dirname(existing)
|
|
248
|
+
end
|
|
249
|
+
base = File.exist?(existing) || File.symlink?(existing) ? File.realpath(existing) : existing
|
|
250
|
+
rest.empty? ? base : File.join(base, *rest)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def safe_canonical(path)
|
|
254
|
+
canonical(File.expand_path(path.to_s))
|
|
255
|
+
rescue StandardError
|
|
256
|
+
File.expand_path(path.to_s)
|
|
257
|
+
end
|
|
258
|
+
|
|
59
259
|
# Validate a declared layer surface fail-closed: unknown entries are
|
|
60
260
|
# refused, and any attempt to declare the record store is refused with
|
|
61
261
|
# its own message (AGT-5: never declarable).
|
|
@@ -160,6 +160,15 @@ module KairosMcp
|
|
|
160
160
|
# switch provider, compress, disable thinking).
|
|
161
161
|
# Falls back to legacy auth_error-only handling when disabled.
|
|
162
162
|
def call_llm_with_fallback(arguments)
|
|
163
|
+
# Every agent phase reasons; none acts. The agent's tools run through
|
|
164
|
+
# this loop's own tool_use handling, under the session's blacklist.
|
|
165
|
+
# Without sandbox_mode a claude_code provider launches `claude -p`
|
|
166
|
+
# in the project root, where it reads the project's instruction
|
|
167
|
+
# files and inherits its permission rules (a project allowing Bash
|
|
168
|
+
# or Write let a planning call run commands outside every gate).
|
|
169
|
+
# The retry and fallback paths reuse these arguments, so they are
|
|
170
|
+
# sandboxed too.
|
|
171
|
+
arguments = arguments.merge('sandbox_mode' => true)
|
|
163
172
|
llm_result = @caller.invoke_tool('llm_call', arguments,
|
|
164
173
|
context: @session.invocation_context)
|
|
165
174
|
parsed = JSON.parse(llm_result.map { |b| b[:text] || b['text'] }.compact.join)
|
|
@@ -48,8 +48,15 @@ the agent to write design drafts to `docs/drafts/` or other project paths.
|
|
|
48
48
|
|
|
49
49
|
### MCP Tool Access
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
`
|
|
51
|
+
KairosChain MCP tools (`context_save`, `knowledge_get`, etc.) are available via
|
|
52
|
+
`invoke_tool` in the Act phase, except what the act route always refuses:
|
|
53
|
+
record-store writers such as `chain_record` (the record judges the act, so the
|
|
54
|
+
act may not write it); configuration writers (`llm_configure`, `mode_hooks_*`,
|
|
55
|
+
`plugin_project`), `hermes_*` and `multi_llm_review*`; `llm_call` with a provider
|
|
56
|
+
that carries tools (codex, cursor); and any step whose path arguments reach the
|
|
57
|
+
KairosChain stores, `.claude/`, `.codex/`, `.mcp.json`, `CLAUDE.md`, `AGENTS.md`
|
|
58
|
+
or anywhere outside the project. LLM calls made by the agent or its act run
|
|
59
|
+
sandboxed. The act route is still deny-based: do not run the agent unattended.
|
|
53
60
|
|
|
54
61
|
## Sub-Agents
|
|
55
62
|
|