dsh-autotier 0.1.0
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/AGENTS.md +93 -0
- package/CHANGELOG.md +85 -0
- package/LICENSE +201 -0
- package/README.es.md +247 -0
- package/README.hi.md +241 -0
- package/README.md +245 -0
- package/README.pt.md +246 -0
- package/README.zh.md +221 -0
- package/SECURITY.md +55 -0
- package/THIRD_PARTY_NOTICES.md +63 -0
- package/cordis.patch.yml +125 -0
- package/docs/preset-row.md +61 -0
- package/docs/supporting-lanes.md +45 -0
- package/lib/index.js +2848 -0
- package/lib/types/command.d.ts +17 -0
- package/lib/types/command.d.ts.map +1 -0
- package/lib/types/config.d.ts +94 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/guard-rules.d.ts +97 -0
- package/lib/types/guard-rules.d.ts.map +1 -0
- package/lib/types/guard.d.ts +70 -0
- package/lib/types/guard.d.ts.map +1 -0
- package/lib/types/index.d.ts +60 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/intent.d.ts +179 -0
- package/lib/types/intent.d.ts.map +1 -0
- package/lib/types/judge.d.ts +50 -0
- package/lib/types/judge.d.ts.map +1 -0
- package/lib/types/policy.d.ts +109 -0
- package/lib/types/policy.d.ts.map +1 -0
- package/lib/types/routing.d.ts +135 -0
- package/lib/types/routing.d.ts.map +1 -0
- package/lib/types/schema.d.ts +134 -0
- package/lib/types/schema.d.ts.map +1 -0
- package/lib/types/service.d.ts +67 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/state.d.ts +46 -0
- package/lib/types/state.d.ts.map +1 -0
- package/lib/types/tiers.d.ts +103 -0
- package/lib/types/tiers.d.ts.map +1 -0
- package/lib/types/tools.d.ts +26 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/types.d.ts +96 -0
- package/lib/types/types.d.ts.map +1 -0
- package/package.json +179 -0
- package/src/command.ts +73 -0
- package/src/config.ts +358 -0
- package/src/guard-rules.ts +303 -0
- package/src/guard.ts +285 -0
- package/src/index.ts +149 -0
- package/src/intent.ts +484 -0
- package/src/judge.ts +150 -0
- package/src/policy.ts +246 -0
- package/src/routing.ts +575 -0
- package/src/schema.ts +295 -0
- package/src/service.ts +131 -0
- package/src/state.ts +134 -0
- package/src/tiers.ts +212 -0
- package/src/tools.ts +128 -0
- package/src/types.ts +120 -0
package/src/guard.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The high-risk guard: a deterministic, tier-conditional denial on
|
|
3
|
+
* `tools/pre-execute`. It protects the cheap tier only — the strong model is
|
|
4
|
+
* the reviewer — and it never weakens `dsh-defend`, the approval service or the
|
|
5
|
+
* sandbox policy.
|
|
6
|
+
*
|
|
7
|
+
* Failure discipline: a guard that throws escalates the agent to the strong
|
|
8
|
+
* tier and lets the call through. Denying every call on a guard bug would turn
|
|
9
|
+
* one defect into a dead session; the escalation keeps the safety property
|
|
10
|
+
* (the strong model reviews) without breaking the turn.
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-autotier/guard
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
16
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-tools'
|
|
18
|
+
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
|
|
19
|
+
import type { ResolvedConfig } from './config.ts'
|
|
20
|
+
import { isCredentialPath, matchCommand, matchPath } from './guard-rules.ts'
|
|
21
|
+
import { noteFailure } from './policy.ts'
|
|
22
|
+
import type { AutotierService } from './service.ts'
|
|
23
|
+
import type { AgentStateStore } from './state.ts'
|
|
24
|
+
import type { TierId } from './types.ts'
|
|
25
|
+
|
|
26
|
+
/** Tool argument keys that carry a shell command. */
|
|
27
|
+
const COMMAND_KEYS = ['command', 'cmd', 'script'] as const
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tool argument keys that carry a filesystem target. `file_path` is specific
|
|
31
|
+
* enough to trust on any tool; the looser keys are only read from tools whose
|
|
32
|
+
* name says they write, so a tool with an unrelated `target` argument (a window
|
|
33
|
+
* handle, a selector) cannot trip the path rules.
|
|
34
|
+
*/
|
|
35
|
+
const STRICT_PATH_KEYS = ['file_path'] as const
|
|
36
|
+
const LOOSE_PATH_KEYS = ['path', 'target', 'file', 'filename'] as const
|
|
37
|
+
|
|
38
|
+
/** Tool names whose `path`-like arguments are filesystem targets. */
|
|
39
|
+
const WRITE_TOOL_PATTERN = /(?:write|edit|patch|create|delete|remove|move|copy|rename|save|apply|mkdir|touch)/iu
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Secret shapes that must never reach a model-visible denial reason or the
|
|
43
|
+
* session log. The matched snippet is the one place user text could leak into
|
|
44
|
+
* the guard's output, so it is redacted before it is composed.
|
|
45
|
+
*/
|
|
46
|
+
const SECRET_PATTERNS: readonly RegExp[] = [
|
|
47
|
+
/(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/giu,
|
|
48
|
+
/\bsk-[A-Za-z0-9._-]{8,}/gu,
|
|
49
|
+
/\bgh[pousr]_[A-Za-z0-9]{8,}/gu,
|
|
50
|
+
/((?:password|passwd|token|secret|api[_-]?key)\s*[=:]\s*)\S+/giu,
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
/** Redact credential-shaped spans from one snippet. */
|
|
54
|
+
export function redactSnippet(text: string): string {
|
|
55
|
+
let redacted = text
|
|
56
|
+
for (const pattern of SECRET_PATTERNS) redacted = redacted.replace(pattern, '$1<redacted>')
|
|
57
|
+
return redacted
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The guard's verdict for one call. */
|
|
61
|
+
export interface GuardVerdict {
|
|
62
|
+
readonly action: 'allow' | 'deny'
|
|
63
|
+
/** Empty for `allow`. */
|
|
64
|
+
readonly reason: string
|
|
65
|
+
/** The rule id that fired, or `''`. */
|
|
66
|
+
readonly rule: string
|
|
67
|
+
/** Which axis matched. */
|
|
68
|
+
readonly axis: 'command' | 'path' | 'protected-path' | 'none'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One call as the guard sees it. */
|
|
72
|
+
export interface GuardInput {
|
|
73
|
+
readonly toolName: string
|
|
74
|
+
readonly args: unknown
|
|
75
|
+
readonly tier: TierId
|
|
76
|
+
readonly config: ResolvedConfig
|
|
77
|
+
/** Resolved sandbox mode, when the policy service is composed. */
|
|
78
|
+
readonly sandboxMode?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Extract the first string-valued key present in `args`. */
|
|
82
|
+
function pick(args: unknown, keys: readonly string[]): string | undefined {
|
|
83
|
+
if (args === null || typeof args !== 'object') return undefined
|
|
84
|
+
const record = args as Record<string, unknown>
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const value = record[key]
|
|
87
|
+
if (typeof value === 'string' && value.length > 0) return value
|
|
88
|
+
}
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether a path sits inside a configured protected surface. Windows resolves
|
|
94
|
+
* case-insensitively and strips trailing dots/spaces from each segment, so the
|
|
95
|
+
* comparison normalizes both before matching; otherwise `PACKAGE.JSON` and
|
|
96
|
+
* `package.json.` would slip past the guard.
|
|
97
|
+
*/
|
|
98
|
+
function isProtectedPath(path: string, protectedPaths: readonly string[]): string | undefined {
|
|
99
|
+
const normalize = (value: string): string => {
|
|
100
|
+
const slashed = value.replace(/\\/gu, '/')
|
|
101
|
+
if (process.platform !== 'win32') return slashed
|
|
102
|
+
return slashed
|
|
103
|
+
.split('/')
|
|
104
|
+
.map(segment => segment.replace(/[. ]+$/u, '').toLowerCase())
|
|
105
|
+
.join('/')
|
|
106
|
+
}
|
|
107
|
+
const normalized = normalize(path)
|
|
108
|
+
for (const entry of protectedPaths) {
|
|
109
|
+
const needle = normalize(entry).replace(/^\.\//u, '')
|
|
110
|
+
if (normalized === needle || normalized.endsWith(`/${needle}`) || normalized.includes(`/${needle}/`)) return entry
|
|
111
|
+
}
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether a command or path is whitelisted (exact, or a path/word boundary prefix). */
|
|
116
|
+
function isWhitelisted(value: string | undefined, toolName: string, whitelist: readonly string[]): boolean {
|
|
117
|
+
if (whitelist.includes(toolName)) return true
|
|
118
|
+
if (value === undefined) return false
|
|
119
|
+
return whitelist.some((entry) => {
|
|
120
|
+
if (value === entry) return true
|
|
121
|
+
const rest = value.slice(entry.length)
|
|
122
|
+
if (!value.startsWith(entry) || rest === '') return false
|
|
123
|
+
// A prefix only whitelists on a real boundary, so `/tmp/scratch` does not
|
|
124
|
+
// cover `/tmp/scratch-malicious`.
|
|
125
|
+
return /^[\s/\\]/u.test(rest)
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Judge one tool call.
|
|
131
|
+
*
|
|
132
|
+
* Order: the guard must be enabled and the executing tier protected, then the
|
|
133
|
+
* whitelist, then the credential/command rules, then the protected-path review
|
|
134
|
+
* rule. A denial names the rule and tells the model to escalate instead of
|
|
135
|
+
* retrying.
|
|
136
|
+
*
|
|
137
|
+
* @param input - the call, the executing tier and the live configuration.
|
|
138
|
+
* @returns the verdict.
|
|
139
|
+
*/
|
|
140
|
+
export function evaluateToolCall(input: GuardInput): GuardVerdict {
|
|
141
|
+
const { config, tier } = input
|
|
142
|
+
if (!config.guard.enabled) return { action: 'allow', reason: '', rule: '', axis: 'none' }
|
|
143
|
+
if (!config.guard.tiers.includes('cheap') || tier !== 'cheap') {
|
|
144
|
+
return { action: 'allow', reason: '', rule: '', axis: 'none' }
|
|
145
|
+
}
|
|
146
|
+
const command = pick(input.args, COMMAND_KEYS)
|
|
147
|
+
// The path axis only applies to tools that write: a `read` of package.json or
|
|
148
|
+
// AGENTS.md is routine cheap-tier work, and upstream's rule set is explicit
|
|
149
|
+
// that reads are never intercepted.
|
|
150
|
+
const path = WRITE_TOOL_PATTERN.test(input.toolName)
|
|
151
|
+
? pick(input.args, STRICT_PATH_KEYS) ?? pick(input.args, LOOSE_PATH_KEYS)
|
|
152
|
+
: undefined
|
|
153
|
+
if (isWhitelisted(command ?? path, input.toolName, config.guard.whitelist)) {
|
|
154
|
+
return { action: 'allow', reason: '', rule: '', axis: 'none' }
|
|
155
|
+
}
|
|
156
|
+
if (path !== undefined) {
|
|
157
|
+
const protectedEntry = isProtectedPath(path, config.guard.protectedPaths)
|
|
158
|
+
if (protectedEntry !== undefined) {
|
|
159
|
+
return {
|
|
160
|
+
action: 'deny',
|
|
161
|
+
rule: 'protected-path',
|
|
162
|
+
axis: 'protected-path',
|
|
163
|
+
reason: `dsh-autotier guard: "${redactSnippet(path)}" is a protected surface (${protectedEntry}). `
|
|
164
|
+
+ 'Modifying it requires the strong tier; report what you intend to change instead of retrying.',
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (command !== undefined) {
|
|
169
|
+
const hit = matchCommand(command)
|
|
170
|
+
if (hit !== null) {
|
|
171
|
+
return {
|
|
172
|
+
action: 'deny',
|
|
173
|
+
rule: hit.rule,
|
|
174
|
+
axis: 'command',
|
|
175
|
+
reason: `dsh-autotier guard: ${redactSnippet(hit.description)}. This command is denied while the cheap tier executes; `
|
|
176
|
+
+ 'the tier will escalate if the task needs it — do not retry the command.',
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (path !== undefined) {
|
|
181
|
+
const hit = matchPath(path)
|
|
182
|
+
if (hit !== null) {
|
|
183
|
+
return {
|
|
184
|
+
action: 'deny',
|
|
185
|
+
rule: hit.rule,
|
|
186
|
+
axis: 'path',
|
|
187
|
+
reason: `dsh-autotier guard: ${redactSnippet(hit.description)}. Credential and key material is denied while the cheap tier `
|
|
188
|
+
+ 'executes; the tier will escalate if the task needs it.',
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { action: 'allow', reason: '', rule: '', axis: 'none' }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The tier the guard believes is executing one call. */
|
|
196
|
+
function tierOf(states: AgentStateStore, agent: Agent | undefined): TierId | undefined {
|
|
197
|
+
if (agent === undefined) return undefined
|
|
198
|
+
const state = states.for(agent)
|
|
199
|
+
// An explicit session override outranks the last applied tier, and an active
|
|
200
|
+
// escalation means the strong tier is reviewing.
|
|
201
|
+
if (state.override === 'strong') return 'strong'
|
|
202
|
+
if (state.override === 'off') return undefined
|
|
203
|
+
if (state.override === 'cheap') return 'cheap'
|
|
204
|
+
if (state.escalation !== undefined && state.escalation.until > Date.now()) return 'strong'
|
|
205
|
+
// Otherwise the tier of the request that produced this step is executing it.
|
|
206
|
+
return state.appliedTier ?? 'cheap'
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Options for {@link registerGuardHook}. */
|
|
210
|
+
export interface GuardHookOptions {
|
|
211
|
+
readonly ctx: Context
|
|
212
|
+
readonly service: AutotierService
|
|
213
|
+
readonly states: AgentStateStore
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Register the `tools/pre-execute` guard.
|
|
218
|
+
*
|
|
219
|
+
* The listener is registered with `{ prepend: true }` so a denial claims the
|
|
220
|
+
* call before any pass-through listener; every allowed call awaits `next()`.
|
|
221
|
+
*
|
|
222
|
+
* @param options - the plugin context, service and state store.
|
|
223
|
+
*/
|
|
224
|
+
export function registerGuardHook({ ctx, service, states }: GuardHookOptions): void {
|
|
225
|
+
ctx.on('tools/pre-execute', async (exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision> => {
|
|
226
|
+
const agent = exec.agent
|
|
227
|
+
try {
|
|
228
|
+
const tier = tierOf(states, agent)
|
|
229
|
+
if (tier === undefined) return next()
|
|
230
|
+
const sandbox = ctx.get('sandboxPolicy') as { resolve(request: { session?: unknown }): { mode?: string } } | undefined
|
|
231
|
+
const sandboxMode = sandbox === undefined || agent === undefined
|
|
232
|
+
? undefined
|
|
233
|
+
: sandbox.resolve({ session: agent.session }).mode
|
|
234
|
+
const verdict = evaluateToolCall({
|
|
235
|
+
toolName: exec.name,
|
|
236
|
+
args: exec.arguments,
|
|
237
|
+
tier,
|
|
238
|
+
config: service.config(),
|
|
239
|
+
...sandboxMode === undefined ? {} : { sandboxMode },
|
|
240
|
+
})
|
|
241
|
+
if (verdict.action === 'allow') return next()
|
|
242
|
+
ctx.logger.warn(
|
|
243
|
+
'dsh-autotier: guard denied %s on the %s tier (rule %s)%s',
|
|
244
|
+
exec.name,
|
|
245
|
+
tier,
|
|
246
|
+
verdict.rule,
|
|
247
|
+
sandboxMode === undefined ? '' : ` [sandbox ${sandboxMode}]`,
|
|
248
|
+
)
|
|
249
|
+
if (agent !== undefined) {
|
|
250
|
+
const state = states.for(agent)
|
|
251
|
+
state.denials += 1
|
|
252
|
+
state.lastDenial = verdict.rule
|
|
253
|
+
}
|
|
254
|
+
return { kind: 'deny', reason: verdict.reason }
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// A broken guard is fail-closed for the call it was judging: the agent is
|
|
257
|
+
// forced onto the strong tier (where the guard does not apply) and the
|
|
258
|
+
// current call is denied. Letting it through would turn one defect into an
|
|
259
|
+
// open door, which is exactly what this guard exists to prevent.
|
|
260
|
+
ctx.logger.error('dsh-autotier: guard malfunction (%o); forcing escalation and denying the call', error)
|
|
261
|
+
if (agent !== undefined) {
|
|
262
|
+
const state = states.for(agent)
|
|
263
|
+
const config = service.config()
|
|
264
|
+
const now = Date.now()
|
|
265
|
+
noteFailure(state, `guard|${String(error)}`, config, now)
|
|
266
|
+
// Force the escalation immediately: one malfunction is enough.
|
|
267
|
+
state.escalation = {
|
|
268
|
+
count: config.escalation.threshold,
|
|
269
|
+
signature: `guard|${String(error)}`,
|
|
270
|
+
until: now + config.escalation.ttlMs,
|
|
271
|
+
rung: (state.escalation?.rung ?? 0) + 1,
|
|
272
|
+
lastAt: now,
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
kind: 'deny',
|
|
277
|
+
reason: 'dsh-autotier guard: the guard itself failed, so this call was denied. '
|
|
278
|
+
+ 'The session is escalated to the strong tier — re-run the call there.',
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}, { prepend: true })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Re-exported for the status surface. */
|
|
285
|
+
export { isCredentialPath }
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-autotier: automatic strong/cheap model-tier routing for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* One user instruction enters, one tier decision comes out — with no manual
|
|
5
|
+
* model switching. Complex intent is planned on the strong tier and implemented
|
|
6
|
+
* on the cheap tier; simple intent is designed and implemented on the cheap
|
|
7
|
+
* tier directly. High-risk tool calls are denied while the cheap tier executes,
|
|
8
|
+
* and repeated failures escalate to the strong tier with a TTL fallback.
|
|
9
|
+
*
|
|
10
|
+
* The routing seam is the official `agent/request` waterfall: a listener
|
|
11
|
+
* registered at load time on the root scope with `{ prepend: true }` runs
|
|
12
|
+
* outermost, awaits `next()` exactly once, and returns a replacement
|
|
13
|
+
* `LlmCallConfig` (provider/model/effort plus the preserved sampling scalars).
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-autotier
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
19
|
+
import { registerTierCommand } from './command.ts'
|
|
20
|
+
import { Config, resolveConfig, validateConfig, type Config as AutotierConfig } from './config.ts'
|
|
21
|
+
import { registerGuardHook } from './guard.ts'
|
|
22
|
+
import { AutotierRouter } from './routing.ts'
|
|
23
|
+
import { AutotierService } from './service.ts'
|
|
24
|
+
import { AgentStateStore, registerTierProjection } from './state.ts'
|
|
25
|
+
import { registerTierTools } from './tools.ts'
|
|
26
|
+
|
|
27
|
+
export { Config, resolveConfig, validateConfig } from './config.ts'
|
|
28
|
+
export type { Config as AutotierConfig, ResolvedConfig } from './config.ts'
|
|
29
|
+
export type {
|
|
30
|
+
AutotierStatus,
|
|
31
|
+
CostMode,
|
|
32
|
+
EffortId,
|
|
33
|
+
RouteDecision,
|
|
34
|
+
RouteSource,
|
|
35
|
+
RoutingMode,
|
|
36
|
+
Scenario,
|
|
37
|
+
TierId,
|
|
38
|
+
TierRoute,
|
|
39
|
+
} from './types.ts'
|
|
40
|
+
export { EFFORT_IDS, ROUTING_MODES, SCENARIOS, TIER_IDS } from './types.ts'
|
|
41
|
+
export { AutotierService } from './service.ts'
|
|
42
|
+
export { AutotierRouter } from './routing.ts'
|
|
43
|
+
export type { RouteProposal, RouteVeto, TierChange } from './routing.ts'
|
|
44
|
+
export { AgentStateStore, registerTierProjection, TIER_PROJECTION_KEY } from './state.ts'
|
|
45
|
+
export {
|
|
46
|
+
classifyIntent,
|
|
47
|
+
compileRules,
|
|
48
|
+
computeSignals,
|
|
49
|
+
evaluateRules,
|
|
50
|
+
fingerprintOf,
|
|
51
|
+
PosteriorTable,
|
|
52
|
+
wilsonLowerBound,
|
|
53
|
+
} from './intent.ts'
|
|
54
|
+
export type { IntentInput, IntentResult, IntentSignals, Posterior, RuleHit } from './intent.ts'
|
|
55
|
+
export {
|
|
56
|
+
attemptBandApplies,
|
|
57
|
+
createRouteState,
|
|
58
|
+
decideTier,
|
|
59
|
+
escalationActive,
|
|
60
|
+
judgeNeeded,
|
|
61
|
+
noteFailure,
|
|
62
|
+
noteFallback,
|
|
63
|
+
noteJudgeCall,
|
|
64
|
+
} from './policy.ts'
|
|
65
|
+
export type { Decision, RouteState } from './policy.ts'
|
|
66
|
+
export {
|
|
67
|
+
advanceFallback,
|
|
68
|
+
classifyFallback,
|
|
69
|
+
effortRank,
|
|
70
|
+
escalationLadder,
|
|
71
|
+
fallbackActive,
|
|
72
|
+
nextEffortStep,
|
|
73
|
+
resolveRoute,
|
|
74
|
+
routeEquals,
|
|
75
|
+
} from './tiers.ts'
|
|
76
|
+
export type { EscalationRung, FallbackClass, FallbackRecord } from './tiers.ts'
|
|
77
|
+
export { JUDGE_LABELS, parseJudgeLabel, resolveJudgeRoute, runJudge } from './judge.ts'
|
|
78
|
+
export type { JudgeOutcome, JudgeRoute } from './judge.ts'
|
|
79
|
+
export { evaluateToolCall, redactSnippet, registerGuardHook } from './guard.ts'
|
|
80
|
+
export type { GuardInput, GuardVerdict } from './guard.ts'
|
|
81
|
+
export {
|
|
82
|
+
HIGH_IMPACT_COMMAND_RULES,
|
|
83
|
+
HIGH_IMPACT_PATH_RULES,
|
|
84
|
+
isCredentialPath,
|
|
85
|
+
matchCommand,
|
|
86
|
+
matchPath,
|
|
87
|
+
} from './guard-rules.ts'
|
|
88
|
+
export type { GuardMatch, GuardRule } from './guard-rules.ts'
|
|
89
|
+
|
|
90
|
+
/** The cordis.yml row id and the plugin name must match. */
|
|
91
|
+
export const name = 'dsh-autotier'
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Hard service dependencies. `sessions` is plural — the service name really is
|
|
95
|
+
* `sessions` (`packages/core/session/src/index.ts` registers `super(ctx,
|
|
96
|
+
* 'sessions')`); declaring a non-existent name would leave this plugin PENDING
|
|
97
|
+
* forever. Every other capability (`agents`, `subagents`, `systemPrompt`,
|
|
98
|
+
* `planMode`, `sessionProjections`, `sandboxPolicy`) is read with `ctx.get()`
|
|
99
|
+
* and degrades when absent.
|
|
100
|
+
*/
|
|
101
|
+
export const inject = ['settings', 'llm', 'tools', 'commands', 'sessions']
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mount the plugin: judge the configuration, register the `autotier` settings
|
|
105
|
+
* namespace, publish the `ctx.autotier` service, and wire the routing listeners,
|
|
106
|
+
* the `/tier` command and the two read-only tools.
|
|
107
|
+
*
|
|
108
|
+
* @param ctx - the plugin context.
|
|
109
|
+
* @param config - the raw row configuration; every field is optional.
|
|
110
|
+
* @throws {Error} when the configuration fails the cross-field judgement.
|
|
111
|
+
*/
|
|
112
|
+
export function apply(ctx: Context, config: AutotierConfig = {}): void {
|
|
113
|
+
// Resolve first so a bad row fails at mount, before any namespace is
|
|
114
|
+
// registered (fail loud, and leave no half-mounted state behind).
|
|
115
|
+
const resolved = resolveConfig(config)
|
|
116
|
+
// Consumer: the plugin reads and validates the shared settings namespace.
|
|
117
|
+
const scope = ctx.settings.register('autotier', Config, {
|
|
118
|
+
base: config,
|
|
119
|
+
applies: 'live',
|
|
120
|
+
validate: (value) => {
|
|
121
|
+
validateConfig(value)
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
const service = new AutotierService(ctx, { scope, config: resolved })
|
|
125
|
+
registerTierProjection(ctx)
|
|
126
|
+
const states = new AgentStateStore()
|
|
127
|
+
new AutotierRouter({ ctx, service, states })
|
|
128
|
+
registerGuardHook({ ctx, service, states })
|
|
129
|
+
registerTierCommand(ctx, service, states)
|
|
130
|
+
registerTierTools(ctx, { service, states })
|
|
131
|
+
if (resolved.guard.interopDefend === 'auto') {
|
|
132
|
+
// Coexistence is deliberate: dsh-defend owns content scanning (injection,
|
|
133
|
+
// jailbreak, secrets) and its own recursive-delete gate; autotier adds
|
|
134
|
+
// tier-conditional denial and escalation guidance. Neither weakens the
|
|
135
|
+
// other, and pass-through discipline keeps both in the chain. dsh-defend
|
|
136
|
+
// provides no service, so this is stated rather than detected.
|
|
137
|
+
ctx.logger.info('dsh-autotier: guard runs alongside dsh-defend when installed (guard.interopDefend=auto)')
|
|
138
|
+
}
|
|
139
|
+
const status = service.status()
|
|
140
|
+
ctx.logger.info(
|
|
141
|
+
'dsh-autotier: mode=%s strong=%s/%s cheap=%s/%s guard=%s',
|
|
142
|
+
status.mode,
|
|
143
|
+
status.tiers.strong.provider,
|
|
144
|
+
status.tiers.strong.model,
|
|
145
|
+
status.tiers.cheap.provider,
|
|
146
|
+
status.tiers.cheap.model,
|
|
147
|
+
status.guard.enabled ? 'on' : 'off',
|
|
148
|
+
)
|
|
149
|
+
}
|