@theokit/agents 13.0.0-next.11 → 13.0.0-next.13
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.
- package/CHANGELOG.md +344 -0
- package/dist/{agent-compiler-DRfBbirH.d.ts → agent-compiler-BztdbNPn.d.ts} +60 -12
- package/dist/auth.d.ts +86 -1
- package/dist/auth.js +36 -0
- package/dist/auth.js.map +1 -1
- package/dist/{bridge-entry-Bk4rgMF8.d.ts → bridge-entry-BOoyL9Sh.d.ts} +4 -4
- package/dist/bridge.d.ts +5 -5
- package/dist/bridge.js +7 -4
- package/dist/{chunk-KTOWSF2S.js → chunk-I7CFQD4E.js} +27 -10
- package/dist/chunk-I7CFQD4E.js.map +1 -0
- package/dist/chunk-M5J3Q6YC.js +17 -0
- package/dist/chunk-M5J3Q6YC.js.map +1 -0
- package/dist/{chunk-RKWCXVYG.js → chunk-MJ6FRILJ.js} +67 -5
- package/dist/chunk-MJ6FRILJ.js.map +1 -0
- package/dist/{chunk-BHW7CSF4.js → chunk-REMEZ6RV.js} +3 -3
- package/dist/{chunk-BHW7CSF4.js.map → chunk-REMEZ6RV.js.map} +1 -1
- package/dist/{chunk-AHLGDLUX.js → chunk-ZNTH3EMC.js} +148 -50
- package/dist/chunk-ZNTH3EMC.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/{define-agent-Cu9uZY7B.d.ts → define-agent-Daz8XjV0.d.ts} +2 -2
- package/dist/{delegation-scoring-CjzhqW2q.d.ts → delegation-scoring-BEwcEnd-.d.ts} +1 -1
- package/dist/hooks.d.ts +0 -32
- package/dist/hooks.js +36 -13
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +80 -15
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/{setting-sources-gate-D2NS1GsB.d.ts → setting-sources-gate-DFu51i50.d.ts} +93 -3
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +1 -1
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +19 -4
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-AHLGDLUX.js.map +0 -1
- package/dist/chunk-KTOWSF2S.js.map +0 -1
- package/dist/chunk-RKWCXVYG.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,349 @@
|
|
|
1
1
|
# @theokit/agents
|
|
2
2
|
|
|
3
|
+
## 13.0.0-next.13
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6b07960: New `grantGate(store, classify)` in `@theokit/agents/auth` — the supported way to put a
|
|
8
|
+
`PermissionStore` in force.
|
|
9
|
+
|
|
10
|
+
The store shipped with a careful grant key, a "deny by default, always" docblock and no reader.
|
|
11
|
+
Measured: `isGranted` had zero callers outside its own unit test, and `PermissionStore` appeared in
|
|
12
|
+
zero files across six sibling repositories. An operator reading `.theokit/tool-permissions.json` to
|
|
13
|
+
learn what an agent may run was reading a control that was not in force — a grant and its revocation
|
|
14
|
+
produced identical behaviour.
|
|
15
|
+
|
|
16
|
+
`grantGate` adapts the store to `pre_tool_call`, which is documented as the only hook with veto
|
|
17
|
+
power and runs before the tool by construction, so a refusal is a refusal before the side effect. No
|
|
18
|
+
new gate and no framework wiring: nothing is enforced unless a consumer attaches the handler, and an
|
|
19
|
+
agent that does not is unaffected.
|
|
20
|
+
|
|
21
|
+
`classify` returns `{ governed: true, query }` **or** `{ governed: false }` — a tagged union whose
|
|
22
|
+
BOTH arms carry the discriminant. A bare `undefined` would say "this tool needs no permission" and
|
|
23
|
+
"I forgot this tool" in the same word, and on a security gate the second must not silently pass.
|
|
24
|
+
|
|
25
|
+
The discriminant is on both arms because the first shape — discriminated by whether a `governed` KEY
|
|
26
|
+
was present — **failed open**: a consumer writing a policy record `{ governed: true, scope }` and
|
|
27
|
+
spreading it into the query got the tool waved through, and so did `governed: undefined`. TypeScript
|
|
28
|
+
does not stop that; excess properties pass freely through a variable or a spread. Fail-closed is now
|
|
29
|
+
measured across `true` / `undefined` / `false` / `null`, and only `false` passes.
|
|
30
|
+
|
|
31
|
+
Named `grantGate`, not `permissionGate`, because `@theokit/sdk` already exports `PermissionGate`,
|
|
32
|
+
`PermissionGateContext`, `PermissionGateDecision`, `PermissionEngine` and `PermissionPlugin`. The
|
|
33
|
+
rename also says something true: this gates on a standing grant the operator made, and the SDK's
|
|
34
|
+
permission engine is a separate system — neither satisfies the other.
|
|
35
|
+
|
|
36
|
+
A classifier that throws DENIES, naming the throw, rather than ending the turn. A corrupt store
|
|
37
|
+
denies with a message that says so — "the permission store could not be read, so no grant applies" —
|
|
38
|
+
distinct from "no standing grant matches", so an operator can tell the two apart.
|
|
39
|
+
|
|
40
|
+
The read error's own text is deliberately NOT in that message: the veto travels to the model, and
|
|
41
|
+
`lastReadError.message` carries the absolute store path and its file mode. It stays on
|
|
42
|
+
`store.lastReadError` for the operator, which is where the actionable remedy (`chmod 600 …`) lives.
|
|
43
|
+
The scope IS still interpolated, so this narrows the exposure rather than eliminating it.
|
|
44
|
+
|
|
45
|
+
**Composing with a `pre_tool_call` you already have**: the field is singular, so assigning the gate
|
|
46
|
+
over an existing handler loses one of the two silently. Compose explicitly —
|
|
47
|
+
`async (ctx) => (await gate(ctx)) ?? (await mine(ctx))`; first veto wins.
|
|
48
|
+
|
|
49
|
+
`PermissionStore`'s docblock now states that the class enforces nothing on its own, and records the
|
|
50
|
+
precedence among the surfaces IN THIS PACKAGE that can refuse a tool — saying plainly that the list
|
|
51
|
+
is not exhaustive, because `@theokit/sdk` has its own permission system that neither knows about
|
|
52
|
+
these nor is known by them.
|
|
53
|
+
|
|
54
|
+
- 5211721: `moderateOutputStream` now delivers the redacted text to the client instead of computing it and
|
|
55
|
+
replaying the original events.
|
|
56
|
+
|
|
57
|
+
It called `runOutputGuards` and discarded the return value, so a guard that redacted correctly had
|
|
58
|
+
its work thrown away: measured against the built artifact, a guard returning `[REDACTED]` delivered
|
|
59
|
+
`sk-abc123`. Only `block` reached the client honestly.
|
|
60
|
+
|
|
61
|
+
**Signature change**: `moderateOutputStream` takes a fourth argument,
|
|
62
|
+
`rebuildText: (text: string, replaced: E) => E`, which builds one event carrying the
|
|
63
|
+
moderated text, given the text-carrying event it replaces. It is required rather than optional —
|
|
64
|
+
optional would let the function compute a redaction it cannot apply, which is the defect being
|
|
65
|
+
removed. Only the caller knows how to construct its own events.
|
|
66
|
+
|
|
67
|
+
**`extractText` MUST match exactly one event kind.** When it matches several, they COLLAPSE INTO
|
|
68
|
+
ONE — measured: `[thinking('CoT: the key is sk-abc'), message(' Here you go.')]` yields a single
|
|
69
|
+
`message` reading `"CoT: the key is [R] Here you go."`, with no `thinking` event surviving. A
|
|
70
|
+
consumer who wants reasoning moderated runs a SECOND pass over that kind rather than widening one
|
|
71
|
+
extractor.
|
|
72
|
+
|
|
73
|
+
`replaced` does not prevent that collapse, and an earlier draft of this entry said it did. What it
|
|
74
|
+
buys is narrower: the surviving event keeps the KIND and metadata of the text-carrying event it
|
|
75
|
+
replaces, instead of being rebuilt from the text alone. `replaced` is ALWAYS the event being replaced.
|
|
76
|
+
It was typed `E | undefined` for a case that cannot happen — a stream where no event carried text
|
|
77
|
+
returns from the absence check before the guards run, so `rebuildText` is not reached at all. It is
|
|
78
|
+
also never a non-text event, because a caller spreading one would emit a duplicate of it.
|
|
79
|
+
|
|
80
|
+
**Second signature change**: a fifth argument, `rebuildResult: (text, result) => R`, applies the
|
|
81
|
+
moderated text to the generator's RETURN value. A stream has two channels and the first release of
|
|
82
|
+
this fix moderated one: the events were redacted while `step.value` — the aggregate `run()` returns
|
|
83
|
+
— still carried the original text. Measured: the guard computed `"the key is [R]"` and
|
|
84
|
+
`run().response` was `"the key is sk-abc123"`, so **`run()`, the primary non-streaming API, kept
|
|
85
|
+
delivering the secret**. That was this fix's own defect one channel over. Required for the same
|
|
86
|
+
reason `rebuildText` is; passed rather than re-derived, because re-running the guards on the
|
|
87
|
+
aggregate would apply a non-idempotent guard twice.
|
|
88
|
+
|
|
89
|
+
When the text is unchanged, the buffered events are replayed verbatim as before. When it changed,
|
|
90
|
+
the **last** text-carrying event is REPLACED by a newly built event carrying the whole moderated
|
|
91
|
+
string, and the earlier text events are dropped. Events carrying no text are never dropped. Note
|
|
92
|
+
"replaced", not "modified": any non-text payload the surviving event carried is lost, as is that of
|
|
93
|
+
the dropped ones — a consumer whose text events carry per-event metadata should moderate one kind
|
|
94
|
+
only, or rebuild from `replaced`. An event whose extracted text is the EMPTY STRING is still
|
|
95
|
+
text-carrying and can be the one replaced.
|
|
96
|
+
|
|
97
|
+
**Known consequence:** when text events straddle a non-text event, their relative order does not
|
|
98
|
+
survive a redaction. Given `text('tok ') , tool_call , text('sk-abc')` the client now receives
|
|
99
|
+
`tool_call , text('tok [R]')` — text that preceded the tool call follows it. Landing on the last
|
|
100
|
+
text-carrying event keeps a trailing terminator in place and keeps any completion claim after the
|
|
101
|
+
work that produced it; what it cannot keep is the interleaving, because the redaction is about the
|
|
102
|
+
whole string and the boundaries are gone by the time it exists. A test pins this so it is found
|
|
103
|
+
here rather than in a transcript that stopped making sense.
|
|
104
|
+
|
|
105
|
+
A guard that rewrites **unconditionally** — a disclaimer appender, a trim, an NFC normaliser —
|
|
106
|
+
takes this path on every stream that DOES carry text. The cost is not proportional to how much the
|
|
107
|
+
guard changed. It does NOT add a text event to a round that produced none — this entry claimed so,
|
|
108
|
+
and the absence check refuses it: a tool-only round yields its tool call and nothing else.
|
|
109
|
+
|
|
110
|
+
- 98b2565: `resolveSettingSources` now returns `readonly GatedSettingSource[]`, and
|
|
111
|
+
`CompiledAgentOptions.settingSources` takes that type — so a setting root no `TrustPosture`
|
|
112
|
+
authorised no longer fits the field.
|
|
113
|
+
|
|
114
|
+
`define-agent.ts` claimed that field "can only ever hold roots that some posture authorized".
|
|
115
|
+
Measured against the emitted `.d.ts`: `setOnce(draft, 'settingSources', ['mdm','team','user','plugins'], 'cap')`
|
|
116
|
+
typechecked **cast-free**. Writing a `Capability` is the documented way to extend the builder, and a
|
|
117
|
+
capability writes the draft directly — so the gate was reachable around, for `project`, the root it
|
|
118
|
+
exists to protect.
|
|
119
|
+
|
|
120
|
+
A brand rather than a runtime check, because the obvious runtime check does not work: reading
|
|
121
|
+
`draft.provenance` to refuse a capability's write would also refuse the LEGITIMATE builder path,
|
|
122
|
+
which writes through `setOnce` too. What differs is where the value came from, and that is what a
|
|
123
|
+
brand carries.
|
|
124
|
+
|
|
125
|
+
**It refuses the accident, not the determined caller** — `as never` defeats it, like every brand.
|
|
126
|
+
Saying so is the point: the comment it replaces claimed an invariant nothing enforced.
|
|
127
|
+
|
|
128
|
+
**New: `settingSources.plugins`**, taking the same `ProjectSettingsGrant` as `project`.
|
|
129
|
+
`PluginsManager.refresh` loads executable bundles from the same cwd-controlled tree, usually
|
|
130
|
+
arriving with the clone, so it gets the same gate and not a weaker one. This is the root the SDK
|
|
131
|
+
genuinely reads and the facade withheld.
|
|
132
|
+
|
|
133
|
+
`team` and `mdm` stay absent, and that is the item's original premise dying under measurement: the
|
|
134
|
+
SDK never reads them — `includesSetting` is called with exactly `"project"` and `"plugins"` — so
|
|
135
|
+
forwarding them would be a capability in the type and nothing at runtime.
|
|
136
|
+
|
|
137
|
+
**Migration**: a consumer constructing `CompiledAgentOptions` by hand must build roots through
|
|
138
|
+
`resolveSettingSources` instead of a string array. That is the supported construction and always was.
|
|
139
|
+
|
|
140
|
+
**Also: a narrowed `claudeCode.import` is now REFUSED on an SDK that cannot read it.**
|
|
141
|
+
|
|
142
|
+
That field's docblock said the narrowed form was "refused at resolve time" below `@theokit/sdk`
|
|
143
|
+
5.4.0. Nothing read a version for it — the only checks in this layer are the hook gate (a different
|
|
144
|
+
option) and a `compatSources` warning that returns silently for any major ≥ 5. So on
|
|
145
|
+
5.0.0 ≤ SDK < 5.4.0, inside this package's declared `^4.52.1 || ^5.0.0`, a narrowed `import` was
|
|
146
|
+
forwarded, dropped by the runtime in silence, and the foreign root was **not read at all** — a
|
|
147
|
+
consumer asking for "the skills but not the hooks" got nothing, which is further from what they
|
|
148
|
+
asked for than the un-narrowed form. `compatSources` landed in 5.0.0 and the narrowing in 5.4.0;
|
|
149
|
+
treating the two versions as one was the defect.
|
|
150
|
+
|
|
151
|
+
`CompatImportUnsupportedError` now refuses, naming both versions and what would otherwise happen.
|
|
152
|
+
It refuses rather than warns because a silent nothing is discovered by wondering why a skill is
|
|
153
|
+
missing. An unreadable version is refused too: "cannot tell" and "is supported" must not collapse.
|
|
154
|
+
|
|
155
|
+
**And `commands` is subtracted before the compat sources reach the SDK.** The two vocabularies
|
|
156
|
+
diverge by one name on purpose — `.claude/commands/*.md` is read by this package and never by the
|
|
157
|
+
SDK — and `setting-sources-gate.ts` prescribed the subtraction as advice to consumers while the
|
|
158
|
+
projection that needed it did not do it. Measured: `import: ['commands']` forwarded a list
|
|
159
|
+
containing zero names the SDK defines, which is its own empty-list case — the exact ambiguity
|
|
160
|
+
`resolveCompatSources` refuses one layer up. A source whose surfaces all belong to this layer
|
|
161
|
+
is now dropped from the SDK's list rather than sent empty.
|
|
162
|
+
|
|
163
|
+
`CompatImportUnsupportedError` is exported from `@theokit/agents/bridge`, so a consumer can catch the
|
|
164
|
+
refusal by class rather than by matching its message.
|
|
165
|
+
|
|
166
|
+
- 2bc5d84: `delegate()` now applies the guardrails its spec declares. It accepted them and never consulted them.
|
|
167
|
+
|
|
168
|
+
Measured against the built artifact with a guard declaring both halves: the input reached the model
|
|
169
|
+
with its injection intact and the caller received `sk-abc123`. `bridge/agent-orchestrator.ts`
|
|
170
|
+
contained **zero** occurrences of `guardrail` — control on the same sweep: `loop/agent-runner.ts`
|
|
171
|
+
has 9 — and called `runReflectiveLoop` bare. The operator had declared guardrails and the run was
|
|
172
|
+
green.
|
|
173
|
+
|
|
174
|
+
This is the same defect class as the streamed-redaction fix in this release, on a sibling public
|
|
175
|
+
API, and it is model-reachable: `tools/delegate-tool.ts` wraps `delegate()`, so an agent can invoke
|
|
176
|
+
a sub-agent whose declared guards do nothing.
|
|
177
|
+
|
|
178
|
+
`checkInput` runs after `onDelegationStart` and `checkOutput` after `onDelegationComplete` — each
|
|
179
|
+
moderating what actually crosses the boundary rather than a string a hook may then rewrite. A
|
|
180
|
+
blocking input guard throws before the model is called at all, so a refused delegation costs
|
|
181
|
+
nothing.
|
|
182
|
+
|
|
183
|
+
`response` only. `toolCalls[].output` is tool output rather than model text, and `agent-runner.ts`
|
|
184
|
+
excludes it from `extractText` on the same reasoning; the docblock says so, because leaving it alone
|
|
185
|
+
should be a decision somebody reads rather than an omission somebody discovers.
|
|
186
|
+
|
|
187
|
+
A spec declaring no guardrails behaves byte-identically.
|
|
188
|
+
|
|
189
|
+
**A guardrail block now crosses the delegate tool as a refusal, not a crash.** `errorCodeOf` mapped
|
|
190
|
+
only the three delegation errors, so `GuardrailViolationError` — reachable from `delegate()` for the
|
|
191
|
+
first time because of this change — hit the "not a delegation outcome, a defect" arm and was
|
|
192
|
+
rethrown, ending the parent's turn. The tool's own description, shipped to the model, promises
|
|
193
|
+
`{ ok: false, error, message }` on a refusal.
|
|
194
|
+
|
|
195
|
+
It crosses as `guardrail_violation` with a FIXED message: `the delegated task was refused by a
|
|
196
|
+
policy guard`. The message travels only for codes on an explicit ALLOWLIST — a budget or a timeout
|
|
197
|
+
is a fact about the work, and the number in it is what the model acts on. A code nobody lists
|
|
198
|
+
withholds, so forgetting is safe in the direction that matters. A guardrail message is not — it reads
|
|
199
|
+
`Guardrail "pii-detector" blocked output: ssn found`, naming the guard and its exact trigger, and a
|
|
200
|
+
model given that learns which words to avoid rather than that it should stop. The operator keeps the
|
|
201
|
+
full typed error, which carries `guardName`, `phase` and `reason`.
|
|
202
|
+
|
|
203
|
+
- 019f828: Output guards now moderate `thinking` events, not only `text_delta`.
|
|
204
|
+
|
|
205
|
+
`AgentRunner` handed `moderateOutputStream` an extractor matching `text_delta` and nothing else,
|
|
206
|
+
while `thinking` is a public `AgentStreamEvent` that reaches the client like any other. Measured: a
|
|
207
|
+
guard declared over the agent's output delivered `thinking "the key is sk-abc123"` verbatim.
|
|
208
|
+
|
|
209
|
+
**This closes one channel and does not close all of them.** `DoneEvent.result` carries the model's
|
|
210
|
+
whole answer and is still unmoderated — measured on the same turn, `text_delta` came out
|
|
211
|
+
`"here: [R]"` while `done.result` came out `"here: sk-abc123"`. `task_progress.text` is a fourth and
|
|
212
|
+
reaches the web wire. A third pass does not extend to them: there is one `done` per round, so a pass
|
|
213
|
+
keyed on it would collapse every round's into one, and they need a different mechanism. Tracked
|
|
214
|
+
separately; stated here because a security note that overstates its coverage is worse than one that
|
|
215
|
+
does not exist.
|
|
216
|
+
|
|
217
|
+
The third channel of a shape fixed twice already in this release — a streamed redaction that was
|
|
218
|
+
computed and discarded, and `delegate()` consulting no guards at all.
|
|
219
|
+
|
|
220
|
+
**Two passes, not one wider extractor.** Widening `extractText` to match both kinds is the obvious
|
|
221
|
+
move and the wrong one: two kinds under one extractor COLLAPSE into a single event, so the reasoning
|
|
222
|
+
would be promoted into a visible one — the moderation creating the disclosure it exists to close.
|
|
223
|
+
Composing two passes is what `moderateOutputStream`'s own docblock prescribes, and each pass seeing
|
|
224
|
+
one kind is what keeps them apart.
|
|
225
|
+
|
|
226
|
+
The visible pass owns the aggregate: `DelegationResult.response` accumulates from `text_delta`
|
|
227
|
+
upstream, so the reasoning pass passes the result through rather than replacing it.
|
|
228
|
+
|
|
229
|
+
A blocking guard on either channel still throws before any event is emitted. An agent with no output
|
|
230
|
+
guard is byte-identical.
|
|
231
|
+
|
|
232
|
+
- 0731584: `resolveCompatSources` now returns `readonly GatedCompatSource[]`, and
|
|
233
|
+
`CompiledAgentOptions.compatSources` takes that type — so a compat source no `TrustPosture`
|
|
234
|
+
authorised no longer fits the field.
|
|
235
|
+
|
|
236
|
+
**BREAKING for hand-built compiled options**, exactly as its sibling was. Build compat sources
|
|
237
|
+
through `resolveCompatSources`.
|
|
238
|
+
|
|
239
|
+
The twin of the `settingSources` brand, and it exists because that fix closed one of the two fields
|
|
240
|
+
one `SettingSourcesSelection` feeds and left the other bare. Measured, with the `settingSources`
|
|
241
|
+
route as the control: the control errored, and `setOnce(draft, 'compatSources', ['claude-code'],
|
|
242
|
+
'cap')` compiled cast-free — while `agent-compiler.ts` told the reader that field "can only hold a
|
|
243
|
+
source some posture granted".
|
|
244
|
+
|
|
245
|
+
It carries more authority than its twin, not less. `applyLocalSources` forwards it to
|
|
246
|
+
`Agent.create({ local: { compatSources } })`, which reads `<cwd>/.claude/` — `hooks.json` included,
|
|
247
|
+
and that executes shell.
|
|
248
|
+
|
|
249
|
+
**Signature narrowing**: `moderateOutputStream`'s `rebuildText` is now
|
|
250
|
+
`(text: string, replaced: E) => E`. It was typed `E | undefined` for a case that cannot happen — a
|
|
251
|
+
stream where no event carried text returns from the absence check before the guards run, so
|
|
252
|
+
`rebuildText` is never reached. The branch handling that case was dead code, and three shipping
|
|
253
|
+
artifacts described it as live.
|
|
254
|
+
|
|
255
|
+
**Fixed**: `isPort` discriminated on the presence of `run`, so an object carrying both `compiled`
|
|
256
|
+
and a `run` — reachable through a spread, which is how targets are built in practice — took the port
|
|
257
|
+
branch and skipped `delegate()` entirely: no declared guardrails, no inherited parent veto, no
|
|
258
|
+
budget clamp. A tie now goes to the spec, because the spec branch is the guarded one.
|
|
259
|
+
|
|
260
|
+
### Patch Changes
|
|
261
|
+
|
|
262
|
+
- bb0f451: A guardrail refusal thrown inside a round is no longer renamed into a delegation failure.
|
|
263
|
+
|
|
264
|
+
`runReflectiveLoop` wrapped any error that was not already a delegation error into
|
|
265
|
+
`DelegationError`, whose message reads `Delegation to agent "X" failed: ${cause.message}`. That code
|
|
266
|
+
is on the delegate tool's message allowlist — a delegation failure's text is a fact about the work —
|
|
267
|
+
so the wrapper carried the guard's own words to the model: `Guardrail "pii-detector" blocked output:
|
|
268
|
+
ssn found`, naming the guard and its exact trigger.
|
|
269
|
+
|
|
270
|
+
Measured through `createDelegateTool` with a consumer-supplied `streamFactory` that throws
|
|
271
|
+
mid-round. `streamFactory` is a public option, so this was reachable rather than theoretical.
|
|
272
|
+
|
|
273
|
+
`GuardrailViolationError` now passes through as itself, alongside the two delegation errors that
|
|
274
|
+
already did, so the tool classifies it `guardrail_violation` and withholds the message.
|
|
275
|
+
|
|
276
|
+
Fixed by classification rather than by suppressing text downstream: a guard refusal is not a
|
|
277
|
+
delegation failure, and a layer that renames an error cannot be expected to maintain a list of what
|
|
278
|
+
the new name must hide.
|
|
279
|
+
|
|
280
|
+
- ac29eff: A permission-gate veto now emits a debug line.
|
|
281
|
+
|
|
282
|
+
`grantGate` refused and emitted nothing — no log, no counter, no debug line. An operator could
|
|
283
|
+
observe the refusal only through the tool result the model received, and the two causes the veto
|
|
284
|
+
message distinguishes ("no standing grant matches" versus "the permission store could not be read,
|
|
285
|
+
so no grant applies") are indistinguishable from there.
|
|
286
|
+
|
|
287
|
+
That is pillar 3 of the wiring triad missing on a refusal seam. `bridge/approval-posture.ts`, the
|
|
288
|
+
sibling gate, already logged through this exact seam.
|
|
289
|
+
|
|
290
|
+
The QUERY is logged — tool, scope, and which of the two causes fired — and the grant is not: a query
|
|
291
|
+
names what an operator needs to diagnose, while the store's contents are the thing being protected.
|
|
292
|
+
A classifier that threw logs as its own event rather than as an ordinary veto, so a consumer-code
|
|
293
|
+
defect is not read as a denied tool.
|
|
294
|
+
|
|
295
|
+
## 13.0.0-next.12
|
|
296
|
+
|
|
297
|
+
### Minor Changes
|
|
298
|
+
|
|
299
|
+
- a7e35bb: A guardrail returning `action: 'redact'` with no replacement `text` now throws
|
|
300
|
+
`MalformedGuardrailResultError` instead of silently redacting nothing.
|
|
301
|
+
|
|
302
|
+
`GuardrailResult.text` is optional, so such a guard compiles and reads like a working one. The
|
|
303
|
+
pipeline tested `r.text !== undefined` and moved on, so the caller received the original text and
|
|
304
|
+
believed a guard had run on it — the operator believing a protection is in place when none is.
|
|
305
|
+
|
|
306
|
+
**This is a behaviour change.** A guard relying on the previous no-op will now throw. That is
|
|
307
|
+
deliberate: the alternative is unredacted output reaching a model because a guard was written wrong.
|
|
308
|
+
`text: ''` is unaffected and always was a real redaction — a guard choosing to erase everything.
|
|
309
|
+
|
|
310
|
+
`MalformedGuardrailResultError` is exported from `@theokit/agents`, carries the guard's name and the
|
|
311
|
+
phase, and is not retryable.
|
|
312
|
+
|
|
313
|
+
This also reaches the streaming path (`moderateOutputStream`), which shares the same pipeline: a
|
|
314
|
+
malformed guard there now throws where it previously continued.
|
|
315
|
+
|
|
316
|
+
### Patch Changes
|
|
317
|
+
|
|
318
|
+
- 2bc27d3: `inheritHooks` no longer lets a member's `transform_tool_result` or `pre_user_send` handler replace
|
|
319
|
+
its parent's — both now chain parent-first, matching the six events that already composed.
|
|
320
|
+
|
|
321
|
+
`inheritHooks` documents its security property as "the parent's refusal is evaluated first, and a
|
|
322
|
+
member can only ever ADD a reason to refuse". For these two events the plain object spread did the
|
|
323
|
+
opposite: a member declaring either handler silently discarded the parent's. The reachable surface is the EXPORTED `inheritHooks`, called with two handler maps.
|
|
324
|
+
`delegate()` passes `undefined` for the member (`agent-orchestrator.ts:175`), so that path composed
|
|
325
|
+
nothing and was never affected — a distinction the first version of this note got wrong.
|
|
326
|
+
|
|
327
|
+
`pre_user_send` composes additively — both contributions reach the model, parent first — because
|
|
328
|
+
`PreUserSendResult` carries only `recalledContext` and the seam exposes no prompt mutation.
|
|
329
|
+
|
|
330
|
+
- eaac7b0: The "will NOT fire" warning for a declared-but-unwired hook event now names where the capability
|
|
331
|
+
already lives, instead of ending "the handler does not exist yet".
|
|
332
|
+
|
|
333
|
+
Two of the three unwired events are served today by purpose-built seams — `Guardrail.checkOutput`
|
|
334
|
+
for `transform_llm_output`, and `createToolHooksPlugin({ processInput })` for `pre_user_send` — so
|
|
335
|
+
the old message told consumers to wait for work that will not come. The third, `on_session_end`, is
|
|
336
|
+
named as genuinely uncovered, with the reason: its handler returns `void` and cannot refuse an
|
|
337
|
+
ending, so wiring it would produce a hook that runs and cannot decide.
|
|
338
|
+
|
|
339
|
+
Nothing is wired. `HOOK_EVENTS`, `WIRED_EVENTS` and `OBSERVATIONAL_EVENTS` keep the same members.
|
|
340
|
+
|
|
341
|
+
- ec899f4: An observational hook handler is now assigned to its own key rather than chosen by comparison.
|
|
342
|
+
|
|
343
|
+
The dispatch loop used a two-branch conditional over a list of two event names, so a third
|
|
344
|
+
observational event would have landed on `post_assistant_reply` — silently, with no test objecting.
|
|
345
|
+
No behaviour changes for the events wired today; the fix removes the trap for the next one added.
|
|
346
|
+
|
|
3
347
|
## 13.0.0-next.11
|
|
4
348
|
|
|
5
349
|
### Patch Changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { InlineSkill, SystemPromptResolver,
|
|
1
|
+
import { InlineSkill, SystemPromptResolver, MemorySettings, SkillsSettings, ContextSettings } from '@theokit/sdk';
|
|
2
2
|
import { TheokitAgentError } from '@theokit/sdk/errors';
|
|
3
3
|
import { R as ReasoningEffort, a as MemoryOptions, P as ProjectContextOptions, M as McpServersMap, H as HumanInTheLoopOptions, C as CheckpointOptions, T as ToolOptions, A as ApprovalOptions, B as BudgetOptions } from './types-C16Wuh9E.js';
|
|
4
|
-
import {
|
|
4
|
+
import { G as GatedSettingSource, a as GatedCompatSource } from './setting-sources-gate-DFu51i50.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* M9 (theokit-ai-first) — guardrail contract + typed errors.
|
|
@@ -36,15 +36,36 @@ interface Guardrail {
|
|
|
36
36
|
}
|
|
37
37
|
/** Which boundary phase a violation happened in. */
|
|
38
38
|
type GuardrailPhase = 'input' | 'output';
|
|
39
|
-
/** Thrown (fail-fast) when a guard returns `action: 'block'`. Typed per error-handling.md. */
|
|
40
39
|
/**
|
|
41
|
-
*
|
|
40
|
+
* A guard declared `redact` and supplied no replacement text.
|
|
42
41
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
42
|
+
* Distinct from {@link GuardrailViolationError} on purpose: that one says the guard REFUSED
|
|
43
|
+
* something, which is a decision working as designed. This says the guard is MALFORMED — it asked
|
|
44
|
+
* for a redaction and gave nothing to redact with, so nothing was redacted.
|
|
45
|
+
*
|
|
46
|
+
* Until B-008 this condition was silent: `pipeline.ts` tested `r.text !== undefined` and moved on, so
|
|
47
|
+
* the caller received the original text and believed a guard had run on it. That is the shape this
|
|
48
|
+
* package's hook engine calls "worse than no hook at all" — a belief in a protection that is not
|
|
49
|
+
* there. Throwing is fail-fast per `rules/error-handling.md § 2`, and the alternative was leaving
|
|
50
|
+
* unredacted output to reach a model because a guard was written wrong.
|
|
51
|
+
*
|
|
52
|
+
* `''` is NOT this case. An empty replacement is a guard choosing to erase everything, which is the
|
|
53
|
+
* strongest redaction available, and treating it as absent would invert the defect.
|
|
54
|
+
*/
|
|
55
|
+
declare class MalformedGuardrailResultError extends TheokitAgentError {
|
|
56
|
+
readonly guardName: string;
|
|
57
|
+
readonly phase: GuardrailPhase;
|
|
58
|
+
readonly name = "MalformedGuardrailResultError";
|
|
59
|
+
constructor(guardName: string, phase: GuardrailPhase);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Thrown (fail-fast) when a guard returns `action: 'block'`. Typed per error-handling.md.
|
|
63
|
+
*
|
|
64
|
+
* M80 — extends {@link TheokitAgentError}, not plain `Error`. `isTransientError` is defined over
|
|
65
|
+
* `TheokitAgentError`, so a class outside that hierarchy is INVISIBLE to it and the only recourse
|
|
66
|
+
* left to a consumer is matching on message text — a regex over an eight-level `cause` chain, which
|
|
67
|
+
* is what one actually wrote. `code` is stable across a rename of the class; `isRetryable` is
|
|
68
|
+
* DECLARED rather than defaulted, because a default would be a retry policy nobody chose.
|
|
48
69
|
*/
|
|
49
70
|
declare class GuardrailViolationError extends TheokitAgentError {
|
|
50
71
|
readonly guardName: string;
|
|
@@ -149,6 +170,33 @@ declare class HookGateUnsupportedError extends TheokitAgentError {
|
|
|
149
170
|
readonly name = "HookGateUnsupportedError";
|
|
150
171
|
constructor(version: string | undefined);
|
|
151
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* A narrowed `import` on an SDK that cannot read it.
|
|
175
|
+
*
|
|
176
|
+
* Refuses rather than warns, and the asymmetry with {@link warnIfSdkCannotReadCompatSources} is
|
|
177
|
+
* deliberate: an unrecognised `compatSources` shape means the foreign root is not read, so a
|
|
178
|
+
* consumer who asked for "the skills but not the hooks" silently gets NOTHING — strictly further
|
|
179
|
+
* from what they asked for than the un-narrowed form, which at least reads something. Failing loud
|
|
180
|
+
* is recoverable; a silent nothing is discovered by wondering why a skill is missing.
|
|
181
|
+
*
|
|
182
|
+
* ## The cost, which this file argues against two functions above
|
|
183
|
+
*
|
|
184
|
+
* An unreadable version is refused too, on the principle {@link assertSdkCanGateHooks} states:
|
|
185
|
+
* "cannot tell" and "is supported" must not collapse. The sibling WARNING takes the opposite view
|
|
186
|
+
* for itself — *"a bundled or vendored SDK may not resolve that subpath, and refusing to create an
|
|
187
|
+
* agent over a diagnostic would be the cure being worse than the disease."*
|
|
188
|
+
*
|
|
189
|
+
* Both are right for what they guard, and the difference is what the check IS. A diagnostic that
|
|
190
|
+
* cannot read a version should stay quiet; a GATE that cannot read one has not established the
|
|
191
|
+
* thing it exists to establish. But the cost is real and belongs here rather than in a reviewer's
|
|
192
|
+
* report: a consumer who bundles the SDK so `@theokit/sdk/package.json` does not resolve cannot
|
|
193
|
+
* create an agent with a narrowed `import`, even on 5.4.0+. Their exit is to omit `import` and read
|
|
194
|
+
* the whole root.
|
|
195
|
+
*/
|
|
196
|
+
declare class CompatImportUnsupportedError extends TheokitAgentError {
|
|
197
|
+
readonly name = "CompatImportUnsupportedError";
|
|
198
|
+
constructor(version: string | undefined);
|
|
199
|
+
}
|
|
152
200
|
|
|
153
201
|
/**
|
|
154
202
|
* Agent compiler — transforms decorator metadata into SDK calls.
|
|
@@ -239,14 +287,14 @@ interface CompiledAgentOptions {
|
|
|
239
287
|
* Projected into `Agent.create({ local: { settingSources } })` by `assembleM8CreateOptions`
|
|
240
288
|
* (merged with `cwd`, decoupled from inline skills). Absent ⇒ inline (code) config only.
|
|
241
289
|
*/
|
|
242
|
-
settingSources?: readonly
|
|
290
|
+
settingSources?: readonly GatedSettingSource[];
|
|
243
291
|
/**
|
|
244
292
|
* Foreign configuration dialects, already authorised (usetheokit/theokit#634).
|
|
245
293
|
*
|
|
246
294
|
* Resolved at compile time by `resolveCompatSources`, exactly like `settingSources`: a value here
|
|
247
295
|
* can only hold a source some posture granted, so the adapter projects rather than decides.
|
|
248
296
|
*/
|
|
249
|
-
compatSources?: readonly
|
|
297
|
+
compatSources?: readonly GatedCompatSource[];
|
|
250
298
|
/**
|
|
251
299
|
* #686 — the consumer's pre-spawn approval gate, forwarded to `Agent.create({ local: { hooks } })`.
|
|
252
300
|
*
|
|
@@ -299,4 +347,4 @@ interface CompiledAgentOptions {
|
|
|
299
347
|
skillsResolver?: SkillsSelection;
|
|
300
348
|
}
|
|
301
349
|
|
|
302
|
-
export { type CompiledAgentOptions as C, type Guardrail as G, type HookApprovalGate as H, type SkillsSelection as S, type ToolWalkResult as T, type CompiledTool as a,
|
|
350
|
+
export { type CompiledAgentOptions as C, type Guardrail as G, type HookApprovalGate as H, MalformedGuardrailResultError as M, type SkillsSelection as S, type ToolWalkResult as T, type CompiledTool as a, CompatImportUnsupportedError as b, CostBudgetExceededError as c, type GuardrailAction as d, type GuardrailPhase as e, type GuardrailResult as f, GuardrailViolationError as g, type HookApprovalRequest as h, HookGateUnsupportedError as i, type SkillsRequestContext as j, type ToolboxWalkResult as k, compileTools as l, resolveEnabledSkills as r };
|
package/dist/auth.d.ts
CHANGED
|
@@ -443,4 +443,89 @@ declare class PermissionStore {
|
|
|
443
443
|
expired(): readonly Grant[];
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Minimal shape mirrored from `@theokit/sdk`.
|
|
448
|
+
*
|
|
449
|
+
* The reason used to read "type-only, no runtime import, so the SDK peer stays optional", and that
|
|
450
|
+
* does not justify mirroring anything: `bridge/hook-handlers.ts` imports these very two SDK types
|
|
451
|
+
* (`PreToolCallContext`, `PreToolCallDecision`) with `import type` and costs the optional peer
|
|
452
|
+
* nothing. A type-only import IS type-only.
|
|
453
|
+
*
|
|
454
|
+
* The real reason is the NAME, and it is written out three paragraphs down: `@theokit/sdk` already
|
|
455
|
+
* exports `PermissionGateContext` with a different shape, so a consumer importing both would get a
|
|
456
|
+
* duplicate identifier or silently the wrong one. What is mirrored here is also a deliberate
|
|
457
|
+
* SUBSET — the fields this gate reads — rather than a copy kept in step with the SDK's.
|
|
458
|
+
*
|
|
459
|
+
* A mirror can drift into a consumer-side compile error that no gate here would see first, so the
|
|
460
|
+
* assignment this docblock instructs a consumer to make is pinned by
|
|
461
|
+
* `tests/type/the-grant-gate-fits-the-hook-it-is-for.test-d.ts`.
|
|
462
|
+
*
|
|
463
|
+
* NOT named `PermissionGateContext`: `@theokit/sdk` already exports a type by that name, with a
|
|
464
|
+
* different shape (`{ toolName, mode }`), alongside `PermissionGate`, `PermissionGateDecision`,
|
|
465
|
+
* `PermissionEngine` and `PermissionPlugin`. A consumer importing both would get a duplicate
|
|
466
|
+
* identifier, or silently the wrong shape.
|
|
467
|
+
*
|
|
468
|
+
* The rename says something true rather than merely avoiding a clash. This gates on a STANDING
|
|
469
|
+
* GRANT the operator made; the SDK's permission engine is a separate system with its own rules, and
|
|
470
|
+
* the two do not consult each other — an engine `allow` does not satisfy a missing grant, and a
|
|
471
|
+
* grant does not satisfy the engine.
|
|
472
|
+
*/
|
|
473
|
+
interface GrantGateContext {
|
|
474
|
+
readonly name: string;
|
|
475
|
+
readonly args: Record<string, unknown>;
|
|
476
|
+
readonly agentId: string;
|
|
477
|
+
readonly runId: string;
|
|
478
|
+
/**
|
|
479
|
+
* The run's resolved permission mode, carried by the real context. Mirrored so the shapes match
|
|
480
|
+
* and so its absence from the gate's logic is a decision rather than an oversight: `bypass` does
|
|
481
|
+
* NOT disable this gate, because a standing grant is the operator's, not the run's.
|
|
482
|
+
*/
|
|
483
|
+
readonly permissionMode?: string;
|
|
484
|
+
}
|
|
485
|
+
/** This tool is deliberately outside the gate's remit — distinct from "I forgot to map it". */
|
|
486
|
+
interface NotGoverned {
|
|
487
|
+
readonly governed: false;
|
|
488
|
+
}
|
|
489
|
+
/** This tool IS governed, by the grant key inside. */
|
|
490
|
+
interface Governed {
|
|
491
|
+
readonly governed: true;
|
|
492
|
+
readonly query: PermissionQuery;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* BOTH arms carry `governed`, and that is the whole point.
|
|
496
|
+
*
|
|
497
|
+
* The first version was `PermissionQuery | NotGoverned` — discriminated by whether the `governed`
|
|
498
|
+
* KEY was present. It failed open, measured against the built artifact: a consumer who writes a
|
|
499
|
+
* policy record `{ governed: true, scope }` and spreads it into the query — the most natural
|
|
500
|
+
* reading of "yes, govern this" — got `undefined` back, and an ungranted destructive command ran.
|
|
501
|
+
* `governed: undefined` did the same. TypeScript does not stop it: a fresh literal with an excess
|
|
502
|
+
* property errors, the same object through a variable or a spread does not.
|
|
503
|
+
*
|
|
504
|
+
* The presence check arrived by obeying a linter. `'governed' in c && c.governed === false` was
|
|
505
|
+
* flagged as "always true given the type", which was true of the DECLARED type and false of every
|
|
506
|
+
* value that reaches it. A type-based lint rule cannot see excess properties, and the safety of a
|
|
507
|
+
* gate is not a type-level fact.
|
|
508
|
+
*
|
|
509
|
+
* With the discriminant on both arms there is no key to be accidentally present: `governed` is
|
|
510
|
+
* declared either way, the check is on its VALUE, and `governed: true` without a `query` is a
|
|
511
|
+
* compile error rather than a silent pass.
|
|
512
|
+
*/
|
|
513
|
+
type Classification = Governed | NotGoverned;
|
|
514
|
+
/**
|
|
515
|
+
* A veto, in the shape `pre_tool_call` returns.
|
|
516
|
+
*
|
|
517
|
+
* Exported because it appears in {@link grantGate}'s public signature — an unexported type there
|
|
518
|
+
* leaves a consumer unable to name the return value. `hooks/secure-store.ts` documents the same
|
|
519
|
+
* case.
|
|
520
|
+
*/
|
|
521
|
+
interface Veto {
|
|
522
|
+
readonly block: true;
|
|
523
|
+
readonly message: string;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* @param store the grants to consult; re-read per call, so a grant made mid-run is seen
|
|
527
|
+
* @param classify maps a tool call to the grant key it needs, or declares it ungoverned
|
|
528
|
+
*/
|
|
529
|
+
declare function grantGate(store: PermissionStore, classify: (ctx: GrantGateContext) => Classification): (ctx: GrantGateContext) => Promise<Veto | undefined>;
|
|
530
|
+
|
|
531
|
+
export { type AgentCredentialInput, type AuthMethod, AuthProvider, CODEX_CLIENT_ID_ENV_VAR, CODEX_PROVIDER, type Classification, CredentialNotFoundError, type ProviderDescriptor as CredentialProviderDescriptor, type CredentialResolution, type CredentialSourcesInput, DEFAULT_PROVIDERS, DeclaredProviderError, type DeviceAuthProvider, type Governed, type Grant, type GrantGateContext, type GrantOptions, type NotGoverned, type PermissionQuery, PermissionStore, type PermissionStoreOptions, type PromptHooks, ProviderKeyMismatchError, ProviderPrefixMismatchError, type ResolveCredentialInput, type SourceOrigin, type Veto, credentialSources, grantGate, loginWithDevice, requireCredential, resolveAgentCredential, resolveCredential };
|
package/dist/auth.js
CHANGED
|
@@ -2,6 +2,9 @@ import {
|
|
|
2
2
|
readSecureJson,
|
|
3
3
|
writeSecureJson
|
|
4
4
|
} from "./chunk-D2EFYZBV.js";
|
|
5
|
+
import {
|
|
6
|
+
debugLog
|
|
7
|
+
} from "./chunk-M5J3Q6YC.js";
|
|
5
8
|
import {
|
|
6
9
|
__name
|
|
7
10
|
} from "./chunk-Z4QWC7IK.js";
|
|
@@ -572,6 +575,38 @@ var PermissionStore = class {
|
|
|
572
575
|
`);
|
|
573
576
|
}
|
|
574
577
|
};
|
|
578
|
+
|
|
579
|
+
// src/auth/permission-gate.ts
|
|
580
|
+
function grantGate(store, classify) {
|
|
581
|
+
return (ctx) => {
|
|
582
|
+
try {
|
|
583
|
+
const classification = classify(ctx);
|
|
584
|
+
if (classification.governed === false) return Promise.resolve(void 0);
|
|
585
|
+
if (store.isGranted(classification.query)) return Promise.resolve(void 0);
|
|
586
|
+
const because = store.lastReadError === void 0 ? "no standing grant matches" : "the permission store could not be read, so no grant applies";
|
|
587
|
+
debugLog("[theokit] permission gate vetoed", {
|
|
588
|
+
tool: ctx.name,
|
|
589
|
+
scope: classification.query.scope,
|
|
590
|
+
because
|
|
591
|
+
});
|
|
592
|
+
return Promise.resolve({
|
|
593
|
+
block: true,
|
|
594
|
+
message: `"${ctx.name}" is not permitted in ${classification.query.scope}: ${because}`
|
|
595
|
+
});
|
|
596
|
+
} catch (cause) {
|
|
597
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
598
|
+
debugLog("[theokit] permission gate could not classify", {
|
|
599
|
+
tool: ctx.name,
|
|
600
|
+
why
|
|
601
|
+
});
|
|
602
|
+
return Promise.resolve({
|
|
603
|
+
block: true,
|
|
604
|
+
message: `permission gate could not classify "${ctx.name}": ${why}`
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
__name(grantGate, "grantGate");
|
|
575
610
|
export {
|
|
576
611
|
AuthProvider,
|
|
577
612
|
CODEX_CLIENT_ID_ENV_VAR,
|
|
@@ -590,6 +625,7 @@ export {
|
|
|
590
625
|
deviceLogin,
|
|
591
626
|
ensureFreshCredential2 as ensureFreshCredential,
|
|
592
627
|
extractAccountId,
|
|
628
|
+
grantGate,
|
|
593
629
|
loginWithDevice,
|
|
594
630
|
openaiDeviceLogin3 as openaiDeviceLogin,
|
|
595
631
|
persistOAuthTokens2 as persistOAuthTokens,
|