opencode-shell-safety 0.2.3 → 0.4.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/README.md +1 -0
- package/package.json +1 -1
- package/src/index.ts +21 -8
package/README.md
CHANGED
|
@@ -78,6 +78,7 @@ Get an API key from [TypeSafe AI](https://docs.typesafe.ai/introduction/quicksta
|
|
|
78
78
|
| `cache.capacity` | Maximum number of cached classification results. |
|
|
79
79
|
| `cache.ttlMs` | Successful classification cache lifetime in milliseconds. |
|
|
80
80
|
| `agents.<name>.enabled` | Enables classification for an agent. |
|
|
81
|
+
| `agents.<name>.fallback` | Optional. Effect applied when classification rejects the command or the request fails: `ask` or `deny`. Defaults to `deny`. |
|
|
81
82
|
| `agents.<name>.thresholds` | Optional per-agent probability thresholds. |
|
|
82
83
|
| `agents.<name>.http` | Optional. Omission permits no HTTP methods or credential hosts. |
|
|
83
84
|
| `agents.<name>.http.methods` | HTTP methods the policy permits. |
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -33,6 +33,8 @@ const ThresholdsSchema = Schema.Struct({
|
|
|
33
33
|
violationProbability: Probability,
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
+
const FallbackEffectSchema = Schema.Literals(["ask", "deny"])
|
|
37
|
+
|
|
36
38
|
const HttpPolicySchema = Schema.Struct({
|
|
37
39
|
methods: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
38
40
|
credentials: Schema.Record(Schema.String, Schema.Array(Schema.String))
|
|
@@ -41,6 +43,7 @@ const HttpPolicySchema = Schema.Struct({
|
|
|
41
43
|
|
|
42
44
|
const AgentPolicySchema = Schema.Struct({
|
|
43
45
|
enabled: Schema.Boolean,
|
|
46
|
+
fallback: Schema.optionalKey(FallbackEffectSchema),
|
|
44
47
|
thresholds: Schema.optionalKey(ThresholdsSchema),
|
|
45
48
|
http: HttpPolicySchema.pipe(Schema.withDecodingDefaultKey(Effect.succeed({ methods: [], credentials: {} }))),
|
|
46
49
|
})
|
|
@@ -75,6 +78,7 @@ export const OptionsSchema = Schema.Struct({
|
|
|
75
78
|
|
|
76
79
|
export type Options = typeof OptionsSchema.Type
|
|
77
80
|
export type AgentPolicy = typeof AgentPolicySchema.Type
|
|
81
|
+
export type FallbackEffect = typeof FallbackEffectSchema.Type
|
|
78
82
|
export type Thresholds = typeof ThresholdsSchema.Type
|
|
79
83
|
|
|
80
84
|
const NoulAnswerSchema = Schema.Struct({
|
|
@@ -161,10 +165,12 @@ export const resolveAgentDefinition = (agent: Agent.Info): ResolvedAgentDefiniti
|
|
|
161
165
|
.map((rule) => ({ resource: rule.resource, effect: rule.effect })),
|
|
162
166
|
})
|
|
163
167
|
|
|
168
|
+
export type ClassificationVerdict = "allow" | "deny"
|
|
169
|
+
|
|
164
170
|
export const permissionFromAssessment = (
|
|
165
171
|
assessment: SafetyAssessment,
|
|
166
172
|
thresholds: Thresholds,
|
|
167
|
-
):
|
|
173
|
+
): ClassificationVerdict => {
|
|
168
174
|
const hasViolation =
|
|
169
175
|
assessment.filesystemViolation.noul >= thresholds.violationProbability ||
|
|
170
176
|
assessment.remoteMutation.noul >= thresholds.violationProbability ||
|
|
@@ -172,6 +178,8 @@ export const permissionFromAssessment = (
|
|
|
172
178
|
return assessment.withinPolicy.noul >= thresholds.allowProbability && !hasViolation ? "allow" : "deny"
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
export const fallbackEffect = (fallback: FallbackEffect | undefined): PermissionEffect => fallback ?? "deny"
|
|
182
|
+
|
|
175
183
|
const noulQuestion = (instructions: string, yes: string, no: string): NoulQuestion => ({
|
|
176
184
|
type: "noul",
|
|
177
185
|
instructions,
|
|
@@ -221,7 +229,6 @@ export const buildRequest = (
|
|
|
221
229
|
|
|
|
222
230
|
|Rules:
|
|
223
231
|
|- Decide whether "shellCommand" complies with those instructions and with "state.policy".
|
|
224
|
-
|- OpenCode invokes this classifier only after permission resolution returns "ask".
|
|
225
232
|
|- An "ask" rule means this classifier must decide from the command's actual effects; it is not a violation.
|
|
226
233
|
|- Shell deny rules remain final.
|
|
227
234
|
|- Apply ordered permission rules with the last matching rule winning.
|
|
@@ -387,16 +394,17 @@ const findPolicy = (options: Options, agent: string): AgentPolicy | undefined =>
|
|
|
387
394
|
const hasShellControlSyntax = (resources: ReadonlyArray<string>): boolean =>
|
|
388
395
|
resources.some((resource) => /[;&|<>`$]/.test(resource))
|
|
389
396
|
|
|
390
|
-
const summary = (assessment: SafetyAssessment, effect: PermissionEffect): string => {
|
|
397
|
+
const summary = (assessment: SafetyAssessment, verdict: ClassificationVerdict, effect: PermissionEffect): string => {
|
|
391
398
|
const classification =
|
|
392
|
-
`Jev shell classification: ${
|
|
399
|
+
`Jev shell classification: ${verdict} (` +
|
|
393
400
|
`withinPolicy=${assessment.withinPolicy.noul.toFixed(3)}, ` +
|
|
394
401
|
`filesystemViolation=${assessment.filesystemViolation.noul.toFixed(3)}, ` +
|
|
395
402
|
`remoteMutation=${assessment.remoteMutation.noul.toFixed(3)}, ` +
|
|
396
403
|
`credentialViolation=${assessment.credentialViolation.noul.toFixed(3)})`
|
|
404
|
+
if (verdict === "allow") return classification
|
|
397
405
|
return effect === "deny"
|
|
398
406
|
? `${classification}. Rewrite the command to comply with your system prompt and permissions, or report the limitation if no compliant form exists.`
|
|
399
|
-
: classification
|
|
407
|
+
: `${classification}. Jev rejected this command.`
|
|
400
408
|
}
|
|
401
409
|
|
|
402
410
|
const classificationSucceeded = (assessment: SafetyAssessment): ClassificationResult => ({ ok: true, assessment })
|
|
@@ -443,19 +451,22 @@ export const createPermissionEvaluator = (
|
|
|
443
451
|
const policy = findPolicy(options, agent)
|
|
444
452
|
if (!policy?.enabled) return Effect.void
|
|
445
453
|
const classifiedEvent = { ...event, agent }
|
|
454
|
+
const fallback = fallbackEffect(policy.fallback)
|
|
446
455
|
|
|
447
456
|
return Effect.gen(function* () {
|
|
448
|
-
event.effect = "deny"
|
|
449
457
|
if (!projectDirectory) {
|
|
458
|
+
event.effect = fallback
|
|
450
459
|
event.message = "Jev could not classify this command because the session directory is unavailable."
|
|
451
460
|
return
|
|
452
461
|
}
|
|
453
462
|
if (!assessmentCache) {
|
|
463
|
+
event.effect = fallback
|
|
454
464
|
event.message = "Jev could not classify this command because no OpenCode Zen credential is available."
|
|
455
465
|
return
|
|
456
466
|
}
|
|
457
467
|
const agentDefinition = yield* resolveAgentDefinition(agent)
|
|
458
468
|
if (!agentDefinition) {
|
|
469
|
+
event.effect = fallback
|
|
459
470
|
event.message = `Jev could not classify this command because the resolved ${agent} definition is unavailable.`
|
|
460
471
|
return
|
|
461
472
|
}
|
|
@@ -468,12 +479,14 @@ export const createPermissionEvaluator = (
|
|
|
468
479
|
Effect.catch((error) => Effect.succeed(classificationFailed(error))),
|
|
469
480
|
)
|
|
470
481
|
if (!result.ok) {
|
|
482
|
+
event.effect = fallback
|
|
471
483
|
event.message = `Jev could not classify this command (${requestFailure(result.error, options)}).`
|
|
472
484
|
return
|
|
473
485
|
}
|
|
474
486
|
|
|
475
|
-
|
|
476
|
-
event.
|
|
487
|
+
const verdict = permissionFromAssessment(result.assessment, policy.thresholds ?? options)
|
|
488
|
+
event.effect = verdict === "allow" ? "allow" : fallback
|
|
489
|
+
event.message = summary(result.assessment, verdict, event.effect)
|
|
477
490
|
})
|
|
478
491
|
}
|
|
479
492
|
})
|