typeclaw 0.3.1 → 0.5.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 +20 -15
- package/auth.schema.json +113 -0
- package/package.json +1 -1
- package/secrets.schema.json +113 -0
- package/src/agent/auth.ts +4 -2
- package/src/agent/index.ts +16 -28
- package/src/agent/model-fallback.ts +127 -0
- package/src/agent/session-meta.ts +1 -1
- package/src/agent/session-origin.ts +3 -2
- package/src/agent/tools/curl-impersonate.ts +300 -0
- package/src/agent/tools/ddg.ts +13 -88
- package/src/agent/tools/webfetch/fetch.ts +105 -2
- package/src/agent/tools/webfetch/tool.ts +4 -0
- package/src/bundled-plugins/agent-browser/shim.ts +47 -0
- package/src/bundled-plugins/backup/subagents.ts +2 -0
- package/src/bundled-plugins/memory/README.md +49 -12
- package/src/bundled-plugins/memory/citation-superset.ts +63 -0
- package/src/bundled-plugins/memory/dreaming.ts +105 -17
- package/src/bundled-plugins/memory/index.ts +2 -2
- package/src/bundled-plugins/memory/memory-logger.ts +45 -26
- package/src/bundled-plugins/memory/strength.ts +127 -0
- package/src/bundled-plugins/memory/topics.ts +75 -0
- package/src/bundled-plugins/security/index.ts +88 -43
- package/src/bundled-plugins/security/permissions.ts +36 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +20 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +12 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +23 -3
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +7 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +6 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +9 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +6 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +7 -0
- package/src/channels/adapters/github/auth-app.ts +120 -0
- package/src/channels/adapters/github/auth-pat.ts +50 -0
- package/src/channels/adapters/github/auth.ts +33 -0
- package/src/channels/adapters/github/channel-resolver.ts +30 -0
- package/src/channels/adapters/github/dedup.ts +26 -0
- package/src/channels/adapters/github/event-allowlist.ts +8 -0
- package/src/channels/adapters/github/fetch-attachment.ts +5 -0
- package/src/channels/adapters/github/history.ts +63 -0
- package/src/channels/adapters/github/inbound.ts +286 -0
- package/src/channels/adapters/github/index.ts +370 -0
- package/src/channels/adapters/github/managed-path.ts +54 -0
- package/src/channels/adapters/github/membership.ts +35 -0
- package/src/channels/adapters/github/outbound.ts +145 -0
- package/src/channels/adapters/github/webhook-register.ts +349 -0
- package/src/channels/manager.ts +94 -9
- package/src/channels/router.ts +194 -28
- package/src/channels/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- package/src/channels/types.ts +3 -1
- package/src/cli/builtins.ts +28 -0
- package/src/cli/channel.ts +511 -25
- package/src/cli/container-command-client.ts +244 -0
- package/src/cli/cron.ts +173 -0
- package/src/cli/host-command-runner.ts +150 -0
- package/src/cli/index.ts +42 -1
- package/src/cli/init.ts +400 -67
- package/src/cli/model.ts +14 -4
- package/src/cli/oauth-callbacks.ts +49 -0
- package/src/cli/plugin-command-help.ts +49 -0
- package/src/cli/plugin-commands-dispatch.ts +112 -0
- package/src/cli/plugin-commands.ts +118 -0
- package/src/cli/provider.ts +3 -20
- package/src/cli/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/config/config.ts +134 -24
- package/src/config/models-mutation.ts +42 -8
- package/src/config/providers-mutation.ts +12 -8
- package/src/container/start.ts +48 -4
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +174 -48
- package/src/cron/index.ts +19 -2
- package/src/cron/list.ts +105 -0
- package/src/cron/scheduler.ts +12 -3
- package/src/cron/schema.ts +11 -3
- package/src/doctor/checks.ts +0 -50
- package/src/init/dockerfile.ts +165 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/hatching.ts +2 -2
- package/src/init/index.ts +519 -12
- package/src/init/oauth-login.ts +17 -3
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +29 -2
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/permissions.ts +24 -7
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +44 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +16 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +144 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +112 -4
- package/src/secrets/index.ts +1 -1
- package/src/secrets/schema.ts +21 -0
- package/src/server/command-runner.ts +476 -0
- package/src/server/index.ts +388 -5
- package/src/shared/index.ts +8 -0
- package/src/shared/protocol.ts +80 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
- package/src/skills/typeclaw-config/SKILL.md +27 -26
- package/src/skills/typeclaw-cron/SKILL.md +234 -3
- package/src/skills/typeclaw-memory/SKILL.md +25 -15
- package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +35 -16
- package/src/skills/typeclaw-plugins/SKILL.md +251 -5
- package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
- package/src/test-helpers/wait-for.ts +50 -0
- package/src/tui/index.ts +70 -7
- package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
- package/src/tunnels/events.ts +14 -0
- package/src/tunnels/index.ts +12 -0
- package/src/tunnels/log-ring.ts +54 -0
- package/src/tunnels/manager.ts +139 -0
- package/src/tunnels/providers/cloudflare-quick.ts +189 -0
- package/src/tunnels/providers/external.ts +53 -0
- package/src/tunnels/quick-url-parser.ts +5 -0
- package/src/tunnels/types.ts +43 -0
- package/src/usage/report.ts +15 -12
- package/typeclaw.schema.json +311 -26
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Strength signals for MEMORY.md topics, derived mechanically from citations.
|
|
2
|
+
//
|
|
3
|
+
// What "strength" means here is structural, not semantic — we measure how
|
|
4
|
+
// many times and over how many distinct days a topic has been reinforced by
|
|
5
|
+
// observation fragments. The reasoning lives in dreaming.ts's system prompt;
|
|
6
|
+
// this file only produces the numbers the prompt will reference.
|
|
7
|
+
//
|
|
8
|
+
// Why distinct days matters more than raw citation count: five fragments on
|
|
9
|
+
// one day == one debugging session that mentioned the same thing five times
|
|
10
|
+
// (a transient burst). Five fragments across five days == a recurring fact
|
|
11
|
+
// the user keeps coming back to (a stable signal). The promotion ladder in
|
|
12
|
+
// the dreaming subagent's prompt is gated on distinct-days, not count, for
|
|
13
|
+
// exactly this reason — see the "spacing effect" note in the PR description.
|
|
14
|
+
//
|
|
15
|
+
// All numbers here are deterministic. The same MEMORY.md parsed against the
|
|
16
|
+
// same `today` always yields the same TopicStrength list. There is no LLM
|
|
17
|
+
// involvement at this layer; the subagent receives these numbers as ground
|
|
18
|
+
// truth and uses them to decide what to merge or demote.
|
|
19
|
+
|
|
20
|
+
import { parseTopics, type Topic } from './topics'
|
|
21
|
+
|
|
22
|
+
export type TopicStrength = {
|
|
23
|
+
heading: string
|
|
24
|
+
citationCount: number
|
|
25
|
+
distinctDays: number
|
|
26
|
+
// ISO date (yyyy-MM-dd) of the most recent citation, or null when the
|
|
27
|
+
// topic has zero citations. Null is distinct from "very old": a topic with
|
|
28
|
+
// no citations at all is a different shape than one whose last citation
|
|
29
|
+
// was a year ago, and the subagent should treat them differently (the
|
|
30
|
+
// former is a typo or a manual edit; the latter is a decayed-but-real
|
|
31
|
+
// topic).
|
|
32
|
+
lastReinforcedDate: string | null
|
|
33
|
+
// Whole-day delta from today to lastReinforcedDate. Null when
|
|
34
|
+
// lastReinforcedDate is null. Negative values are clamped to 0 (a citation
|
|
35
|
+
// dated in the future is treated as "today" — the only way this happens
|
|
36
|
+
// is a clock skew between memory-logger and the dreaming run, and the
|
|
37
|
+
// subagent shouldn't be punished for the runtime's confusion).
|
|
38
|
+
daysSinceLastReinforced: number | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function computeTopicStrengths(memoryText: string, today: string): TopicStrength[] {
|
|
42
|
+
const topics = parseTopics(memoryText)
|
|
43
|
+
return topics.map((topic) => computeOneTopicStrength(topic, today))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function computeOneTopicStrength(topic: Topic, today: string): TopicStrength {
|
|
47
|
+
const citationCount = topic.citations.length
|
|
48
|
+
const distinctDates = new Set(topic.citations.map((c) => c.date))
|
|
49
|
+
const distinctDays = distinctDates.size
|
|
50
|
+
const lastReinforcedDate = pickLatestDate([...distinctDates])
|
|
51
|
+
const daysSinceLastReinforced = lastReinforcedDate ? daysBetween(today, lastReinforcedDate) : null
|
|
52
|
+
return {
|
|
53
|
+
heading: topic.heading,
|
|
54
|
+
citationCount,
|
|
55
|
+
distinctDays,
|
|
56
|
+
lastReinforcedDate,
|
|
57
|
+
daysSinceLastReinforced,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pickLatestDate(dates: readonly string[]): string | null {
|
|
62
|
+
if (dates.length === 0) return null
|
|
63
|
+
let latest = dates[0]!
|
|
64
|
+
for (let i = 1; i < dates.length; i++) {
|
|
65
|
+
const candidate = dates[i]!
|
|
66
|
+
if (candidate.localeCompare(latest) > 0) latest = candidate
|
|
67
|
+
}
|
|
68
|
+
return latest
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Whole-day delta in UTC between two yyyy-MM-dd strings. Date.UTC parses each
|
|
72
|
+
// date as midnight UTC, so the difference is always an integer count of
|
|
73
|
+
// 86_400_000ms windows regardless of timezone or DST. Returns 0 for invalid
|
|
74
|
+
// inputs (treats the topic as "fresh" rather than throwing — defensive
|
|
75
|
+
// because both inputs are produced by the runtime, but a corrupted MEMORY.md
|
|
76
|
+
// citation date is the kind of thing we want to fail open on).
|
|
77
|
+
function daysBetween(today: string, earlier: string): number {
|
|
78
|
+
const todayMs = parseIsoDateUtc(today)
|
|
79
|
+
const earlierMs = parseIsoDateUtc(earlier)
|
|
80
|
+
if (todayMs === null || earlierMs === null) return 0
|
|
81
|
+
const deltaDays = Math.floor((todayMs - earlierMs) / 86_400_000)
|
|
82
|
+
return deltaDays < 0 ? 0 : deltaDays
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseIsoDateUtc(date: string): number | null {
|
|
86
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date)
|
|
87
|
+
if (!match) return null
|
|
88
|
+
const year = Number.parseInt(match[1]!, 10)
|
|
89
|
+
const month = Number.parseInt(match[2]!, 10)
|
|
90
|
+
const day = Number.parseInt(match[3]!, 10)
|
|
91
|
+
const ms = Date.UTC(year, month - 1, day)
|
|
92
|
+
return Number.isFinite(ms) ? ms : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Render the strength signals as a markdown table the dreaming subagent can
|
|
96
|
+
// read at the top of its user prompt. Returns an empty string when the
|
|
97
|
+
// topic list is empty so the caller can prepend it unconditionally.
|
|
98
|
+
//
|
|
99
|
+
// Column choices: heading first because it's the human-recognizable handle;
|
|
100
|
+
// `cites` and `days` are short enough to align nicely; `last` carries the
|
|
101
|
+
// date itself so the subagent can compare to today without re-doing the
|
|
102
|
+
// arithmetic. Headings are truncated to keep the table readable when a
|
|
103
|
+
// topic was given a long sentence-shaped heading — the citation count is
|
|
104
|
+
// still accurate, only the display label is shortened.
|
|
105
|
+
export function renderTopicStrengthsTable(strengths: readonly TopicStrength[]): string {
|
|
106
|
+
if (strengths.length === 0) return ''
|
|
107
|
+
const rows = strengths.map((s) => ({
|
|
108
|
+
heading: truncateHeading(s.heading || '(untitled)'),
|
|
109
|
+
cites: String(s.citationCount),
|
|
110
|
+
days: String(s.distinctDays),
|
|
111
|
+
last: s.lastReinforcedDate ?? '—',
|
|
112
|
+
ageDays: s.daysSinceLastReinforced === null ? '—' : String(s.daysSinceLastReinforced),
|
|
113
|
+
}))
|
|
114
|
+
const lines = ['| topic | cites | days | last reinforced | age (d) |', '| --- | ---: | ---: | --- | ---: |']
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
lines.push(`| ${row.heading} | ${row.cites} | ${row.days} | ${row.last} | ${row.ageDays} |`)
|
|
117
|
+
}
|
|
118
|
+
return lines.join('\n')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const HEADING_MAX_CHARS = 60
|
|
122
|
+
|
|
123
|
+
function truncateHeading(heading: string): string {
|
|
124
|
+
const escaped = heading.replace(/\|/g, '\\|')
|
|
125
|
+
if (escaped.length <= HEADING_MAX_CHARS) return escaped
|
|
126
|
+
return `${escaped.slice(0, HEADING_MAX_CHARS - 1)}…`
|
|
127
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Topic-aware parser for MEMORY.md. The dreaming subagent writes MEMORY.md as
|
|
2
|
+
// a flat list of level-2 topic headings (`## <topic>`), each followed by a
|
|
3
|
+
// conclusion paragraph and a `fragments:` bullet list of citations. The
|
|
4
|
+
// citation parser in citations.ts is global (every citation in the file);
|
|
5
|
+
// this module attributes citations to their owning topic so the dreaming
|
|
6
|
+
// subagent can see per-topic strength signals (citation count, distinct
|
|
7
|
+
// reinforcement days, recency) on its next run.
|
|
8
|
+
//
|
|
9
|
+
// Format assumptions match what dreaming.ts's DREAMING_SYSTEM_PROMPT teaches:
|
|
10
|
+
// - First line is `# Memory` (an h1). Treated as a non-topic header.
|
|
11
|
+
// - Topics are h2s (`## <topic>`). Anything below an h2 and above the next
|
|
12
|
+
// h2 (or EOF) belongs to that topic.
|
|
13
|
+
// - Citations in a topic's body — wherever they appear, bullet-list or
|
|
14
|
+
// inline prose — count toward that topic's strength.
|
|
15
|
+
// - Content above the first h2 (e.g. preamble after `# Memory`) is
|
|
16
|
+
// attributed to no topic and its citations are dropped from the per-topic
|
|
17
|
+
// aggregation. parseCitations from citations.ts still picks them up if
|
|
18
|
+
// anything downstream needs the global view.
|
|
19
|
+
//
|
|
20
|
+
// The parser is intentionally permissive: it never throws on malformed
|
|
21
|
+
// MEMORY.md. A subagent that writes a header with no body or a topic with no
|
|
22
|
+
// citations still parses cleanly with an empty `citations` array. The
|
|
23
|
+
// strength layer then treats those topics as "weak" — which is the right
|
|
24
|
+
// behavior, since they ARE weak.
|
|
25
|
+
|
|
26
|
+
import { type Citation, parseCitations } from './citations'
|
|
27
|
+
|
|
28
|
+
export type Topic = {
|
|
29
|
+
// The heading text after `## ` with surrounding whitespace trimmed. Empty
|
|
30
|
+
// string is allowed (`## ` with no title) so a malformed write still
|
|
31
|
+
// round-trips through the parser; the strength layer surfaces empty
|
|
32
|
+
// headings as themselves so the subagent can clean them up.
|
|
33
|
+
heading: string
|
|
34
|
+
// Citations attached to this topic, deduplicated per `(date, fragmentId)`.
|
|
35
|
+
// The dedupe happens inside parseCitations (which returns a Set of ids per
|
|
36
|
+
// date), so a fragment cited twice in one topic — once in inline prose,
|
|
37
|
+
// once in the fragments: block — counts only once toward strength signals.
|
|
38
|
+
// Order is by date insertion in parseCitations, not by appearance in the
|
|
39
|
+
// topic body; consumers that need appearance order should re-parse.
|
|
40
|
+
citations: Citation[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const HEADING_LEVEL_2 = /^##\s+(.*)$/
|
|
44
|
+
|
|
45
|
+
// Split MEMORY.md into ordered topics with their citations attached. Returns
|
|
46
|
+
// an empty array when no `## ` heading appears.
|
|
47
|
+
export function parseTopics(text: string): Topic[] {
|
|
48
|
+
const lines = text.split('\n')
|
|
49
|
+
const topics: Topic[] = []
|
|
50
|
+
let current: { heading: string; body: string[] } | undefined
|
|
51
|
+
|
|
52
|
+
const flush = (): void => {
|
|
53
|
+
if (!current) return
|
|
54
|
+
const bodyText = current.body.join('\n')
|
|
55
|
+
const grouped = parseCitations(bodyText)
|
|
56
|
+
const citations: Citation[] = []
|
|
57
|
+
for (const [date, ids] of grouped) {
|
|
58
|
+
for (const fragmentId of ids) citations.push({ date, fragmentId })
|
|
59
|
+
}
|
|
60
|
+
topics.push({ heading: current.heading, citations })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const match = HEADING_LEVEL_2.exec(line)
|
|
65
|
+
if (match) {
|
|
66
|
+
flush()
|
|
67
|
+
current = { heading: (match[1] ?? '').trim(), body: [] }
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if (current) current.body.push(line)
|
|
71
|
+
}
|
|
72
|
+
flush()
|
|
73
|
+
|
|
74
|
+
return topics
|
|
75
|
+
}
|
|
@@ -1,53 +1,88 @@
|
|
|
1
1
|
import { definePlugin } from '@/plugin'
|
|
2
2
|
|
|
3
|
-
import { SECURITY_PERMISSIONS } from './permissions'
|
|
4
|
-
import type { SecurityPermission } from './permissions'
|
|
5
|
-
import {
|
|
6
|
-
|
|
3
|
+
import { HIGH_TIER_PER_GUARD_PERMISSIONS, SECURITY_PERMISSIONS, SEVERITY_PERMISSION } from './permissions'
|
|
4
|
+
import type { SecurityPermission, SecuritySeverity } from './permissions'
|
|
5
|
+
import {
|
|
6
|
+
GUARD_GIT_EXFIL_SEVERITY,
|
|
7
|
+
GUARD_GIT_REMOTE_TAINTED_SEVERITY,
|
|
8
|
+
checkGitExfilGuard,
|
|
9
|
+
checkGitRemoteTaintedGuard,
|
|
10
|
+
recordGitRemoteTaintIfAny,
|
|
11
|
+
} from './policies/git-exfil'
|
|
12
|
+
import { GUARD_OUTBOUND_SECRET_SEVERITY, checkOutboundSecretGuard } from './policies/outbound-secret-scan'
|
|
7
13
|
import { applyPromptInjectionDefense } from './policies/prompt-injection'
|
|
8
14
|
import { clearSessionTaints } from './policies/remote-taint-state'
|
|
9
|
-
import { checkSecretExfilBashGuard } from './policies/secret-exfil-bash'
|
|
10
|
-
import { checkSecretExfilReadGuard } from './policies/secret-exfil-read'
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
import { GUARD_SECRET_EXFIL_BASH_SEVERITY, checkSecretExfilBashGuard } from './policies/secret-exfil-bash'
|
|
16
|
+
import { GUARD_SECRET_EXFIL_READ_SEVERITY, checkSecretExfilReadGuard } from './policies/secret-exfil-read'
|
|
17
|
+
import {
|
|
18
|
+
GUARD_SESSION_SEARCH_SECRETS_SEVERITY,
|
|
19
|
+
checkSessionSearchSecretsGuard,
|
|
20
|
+
} from './policies/session-search-secrets'
|
|
21
|
+
import { GUARD_SSRF_SEVERITY, checkSsrfGuard } from './policies/ssrf'
|
|
22
|
+
import { GUARD_SYSTEM_PROMPT_LEAK_SEVERITY, checkSystemPromptLeakGuard } from './policies/system-prompt-leak'
|
|
14
23
|
import type { SecurityBlock } from './policy'
|
|
15
24
|
|
|
16
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
HIGH_TIER_PER_GUARD_PERMISSIONS,
|
|
27
|
+
SECURITY_PERMISSIONS,
|
|
28
|
+
type SecurityPermission,
|
|
29
|
+
type SecuritySeverity,
|
|
30
|
+
SEVERITY_PERMISSION,
|
|
31
|
+
} from './permissions'
|
|
17
32
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
// Per-guard permission strings only — tier strings are deliberately
|
|
34
|
+
// absent. Block messages name the per-guard permission AND the tier
|
|
35
|
+
// permission separately (see withPermissionHint); the per-guard hint
|
|
36
|
+
// table answers "which roles carry THIS specific bypass by default."
|
|
37
|
+
type PerGuardSecurityPermission = Exclude<
|
|
38
|
+
SecurityPermission,
|
|
39
|
+
| typeof SECURITY_PERMISSIONS.bypassLow
|
|
40
|
+
| typeof SECURITY_PERMISSIONS.bypassMedium
|
|
41
|
+
| typeof SECURITY_PERMISSIONS.bypassHigh
|
|
42
|
+
>
|
|
43
|
+
|
|
44
|
+
// The satisfies clause forces exhaustive coverage of per-guard
|
|
45
|
+
// permissions at compile time — adding a new SECURITY_PERMISSIONS entry
|
|
46
|
+
// (other than a new tier string) without a hint here is a type error,
|
|
47
|
+
// not a silent fallback.
|
|
26
48
|
const BYPASS_ROLE_HINT = {
|
|
27
|
-
[SECURITY_PERMISSIONS.bypassSecretExfilBash]:
|
|
28
|
-
|
|
29
|
-
[SECURITY_PERMISSIONS.
|
|
30
|
-
|
|
31
|
-
[SECURITY_PERMISSIONS.
|
|
32
|
-
|
|
33
|
-
[SECURITY_PERMISSIONS.
|
|
34
|
-
[SECURITY_PERMISSIONS.
|
|
35
|
-
|
|
49
|
+
[SECURITY_PERMISSIONS.bypassSecretExfilBash]:
|
|
50
|
+
'only owner has it by default (medium tier; trusted does NOT carry this — operators can grant `security.bypass.secretExfilBash` explicitly in roles.trusted.permissions[] if they want the pre-PR ergonomics back)',
|
|
51
|
+
[SECURITY_PERMISSIONS.bypassGitExfil]:
|
|
52
|
+
'NOBODY has it by default — high tier requires per-call ack from every role, including owner. Operators can grant `security.bypass.gitExfil` explicitly in roles.<role>.permissions[] to re-open the auto-bypass for one role.',
|
|
53
|
+
[SECURITY_PERMISSIONS.bypassGitRemoteTainted]:
|
|
54
|
+
'NOBODY has it by default — high tier requires per-call ack from every role. Even an operator-granted `security.bypass.gitExfil` does NOT bypass this second-step taint check (the recorder still fires for the first step, so the push is still gated).',
|
|
55
|
+
[SECURITY_PERMISSIONS.bypassSecretExfilRead]: 'only owner has it by default (medium tier)',
|
|
56
|
+
[SECURITY_PERMISSIONS.bypassSsrf]: 'only owner has it by default (medium tier)',
|
|
57
|
+
[SECURITY_PERMISSIONS.bypassSessionSearchSecrets]: 'only owner has it by default (medium tier)',
|
|
58
|
+
[SECURITY_PERMISSIONS.bypassSystemPromptLeak]:
|
|
59
|
+
'NOBODY has it by default — high tier requires per-call ack from every role, including owner.',
|
|
60
|
+
[SECURITY_PERMISSIONS.bypassOutboundSecret]:
|
|
61
|
+
'NOBODY has it by default — high tier requires per-call ack from every role, including owner. The audience-leak rule: even owner posting to a public channel must not silently include credentials.',
|
|
62
|
+
} as const satisfies Record<PerGuardSecurityPermission, string>
|
|
36
63
|
|
|
37
64
|
function withPermissionHint(
|
|
38
65
|
result: SecurityBlock | undefined,
|
|
39
|
-
permission:
|
|
66
|
+
permission: PerGuardSecurityPermission,
|
|
67
|
+
severity: SecuritySeverity,
|
|
40
68
|
): SecurityBlock | undefined {
|
|
41
69
|
if (!result) return result
|
|
42
|
-
const
|
|
70
|
+
const perGuardHint = BYPASS_ROLE_HINT[permission]
|
|
71
|
+
const tierPerm = SEVERITY_PERMISSION[severity]
|
|
43
72
|
return {
|
|
44
73
|
block: true,
|
|
45
|
-
reason: `${result.reason} Or run as a role carrying \`${permission}\` (${
|
|
74
|
+
reason: `${result.reason} Or run as a role carrying \`${permission}\` (${perGuardHint}) or the tier permission \`${tierPerm}\`; see the \`typeclaw-permissions\` skill.`,
|
|
46
75
|
}
|
|
47
76
|
}
|
|
48
77
|
|
|
49
78
|
export default definePlugin({
|
|
50
79
|
permissions: Object.values(SECURITY_PERMISSIONS),
|
|
80
|
+
// High-tier per-guard strings AND the `security.bypass.high` tier
|
|
81
|
+
// string itself are excluded from the owner-wildcard expansion. Owner
|
|
82
|
+
// still has the wildcard sentinel (so future low/medium plugin-
|
|
83
|
+
// contributed bypasses keep auto-flowing to owner), but audience-leak
|
|
84
|
+
// guards require either per-call ack or an explicit operator grant.
|
|
85
|
+
ownerWildcardExclusions: [...HIGH_TIER_PER_GUARD_PERMISSIONS, SECURITY_PERMISSIONS.bypassHigh],
|
|
51
86
|
plugin: async (ctx) => ({
|
|
52
87
|
hooks: {
|
|
53
88
|
'session.prompt': async (event) => {
|
|
@@ -55,68 +90,78 @@ export default definePlugin({
|
|
|
55
90
|
},
|
|
56
91
|
'tool.before': async (event) => {
|
|
57
92
|
const can = (perm: string) => ctx.permissions.has(event.origin, perm)
|
|
93
|
+
const canBypass = (severity: SecuritySeverity, perGuardPerm: string): boolean =>
|
|
94
|
+
can(SEVERITY_PERMISSION[severity]) || can(perGuardPerm)
|
|
58
95
|
|
|
59
96
|
// Taint-recording runs FIRST, independently of the gitExfil guard.
|
|
60
97
|
// The gitRemoteTainted defense depends on it. We pass through
|
|
61
|
-
// `permittedBypass` for actors who can skip gitExfil via
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// change must be remembered
|
|
98
|
+
// `permittedBypass` for actors who can skip gitExfil (via either the
|
|
99
|
+
// per-guard permission or the medium-tier permission) so the
|
|
100
|
+
// recorder still fires for them — an acked or permission-bypassed
|
|
101
|
+
// command will actually run, so its remote change must be remembered.
|
|
65
102
|
recordGitRemoteTaintIfAny({
|
|
66
103
|
tool: event.tool,
|
|
67
104
|
args: event.args,
|
|
68
105
|
sessionId: event.sessionId,
|
|
69
|
-
permittedBypass:
|
|
106
|
+
permittedBypass: canBypass(GUARD_GIT_EXFIL_SEVERITY, SECURITY_PERMISSIONS.bypassGitExfil),
|
|
70
107
|
})
|
|
71
108
|
|
|
72
109
|
const checks: (SecurityBlock | undefined)[] = [
|
|
73
|
-
|
|
110
|
+
canBypass(GUARD_GIT_REMOTE_TAINTED_SEVERITY, SECURITY_PERMISSIONS.bypassGitRemoteTainted)
|
|
74
111
|
? undefined
|
|
75
112
|
: withPermissionHint(
|
|
76
113
|
checkGitRemoteTaintedGuard({ tool: event.tool, args: event.args, sessionId: event.sessionId }),
|
|
77
114
|
SECURITY_PERMISSIONS.bypassGitRemoteTainted,
|
|
115
|
+
GUARD_GIT_REMOTE_TAINTED_SEVERITY,
|
|
78
116
|
),
|
|
79
|
-
|
|
117
|
+
canBypass(GUARD_SECRET_EXFIL_BASH_SEVERITY, SECURITY_PERMISSIONS.bypassSecretExfilBash)
|
|
80
118
|
? undefined
|
|
81
119
|
: withPermissionHint(
|
|
82
120
|
checkSecretExfilBashGuard({ tool: event.tool, args: event.args }),
|
|
83
121
|
SECURITY_PERMISSIONS.bypassSecretExfilBash,
|
|
122
|
+
GUARD_SECRET_EXFIL_BASH_SEVERITY,
|
|
84
123
|
),
|
|
85
|
-
|
|
124
|
+
canBypass(GUARD_GIT_EXFIL_SEVERITY, SECURITY_PERMISSIONS.bypassGitExfil)
|
|
86
125
|
? undefined
|
|
87
126
|
: withPermissionHint(
|
|
88
127
|
checkGitExfilGuard({ tool: event.tool, args: event.args, sessionId: event.sessionId }),
|
|
89
128
|
SECURITY_PERMISSIONS.bypassGitExfil,
|
|
129
|
+
GUARD_GIT_EXFIL_SEVERITY,
|
|
90
130
|
),
|
|
91
|
-
|
|
131
|
+
canBypass(GUARD_SECRET_EXFIL_READ_SEVERITY, SECURITY_PERMISSIONS.bypassSecretExfilRead)
|
|
92
132
|
? undefined
|
|
93
133
|
: withPermissionHint(
|
|
94
134
|
checkSecretExfilReadGuard({ tool: event.tool, args: event.args }),
|
|
95
135
|
SECURITY_PERMISSIONS.bypassSecretExfilRead,
|
|
136
|
+
GUARD_SECRET_EXFIL_READ_SEVERITY,
|
|
96
137
|
),
|
|
97
|
-
|
|
138
|
+
canBypass(GUARD_SSRF_SEVERITY, SECURITY_PERMISSIONS.bypassSsrf)
|
|
98
139
|
? undefined
|
|
99
140
|
: withPermissionHint(
|
|
100
141
|
checkSsrfGuard({ tool: event.tool, args: event.args }),
|
|
101
142
|
SECURITY_PERMISSIONS.bypassSsrf,
|
|
143
|
+
GUARD_SSRF_SEVERITY,
|
|
102
144
|
),
|
|
103
|
-
|
|
145
|
+
canBypass(GUARD_SESSION_SEARCH_SECRETS_SEVERITY, SECURITY_PERMISSIONS.bypassSessionSearchSecrets)
|
|
104
146
|
? undefined
|
|
105
147
|
: withPermissionHint(
|
|
106
148
|
checkSessionSearchSecretsGuard({ tool: event.tool, args: event.args }),
|
|
107
149
|
SECURITY_PERMISSIONS.bypassSessionSearchSecrets,
|
|
150
|
+
GUARD_SESSION_SEARCH_SECRETS_SEVERITY,
|
|
108
151
|
),
|
|
109
|
-
|
|
152
|
+
canBypass(GUARD_SYSTEM_PROMPT_LEAK_SEVERITY, SECURITY_PERMISSIONS.bypassSystemPromptLeak)
|
|
110
153
|
? undefined
|
|
111
154
|
: withPermissionHint(
|
|
112
155
|
checkSystemPromptLeakGuard({ tool: event.tool, args: event.args }),
|
|
113
156
|
SECURITY_PERMISSIONS.bypassSystemPromptLeak,
|
|
157
|
+
GUARD_SYSTEM_PROMPT_LEAK_SEVERITY,
|
|
114
158
|
),
|
|
115
|
-
|
|
159
|
+
canBypass(GUARD_OUTBOUND_SECRET_SEVERITY, SECURITY_PERMISSIONS.bypassOutboundSecret)
|
|
116
160
|
? undefined
|
|
117
161
|
: withPermissionHint(
|
|
118
162
|
checkOutboundSecretGuard({ tool: event.tool, args: event.args }),
|
|
119
163
|
SECURITY_PERMISSIONS.bypassOutboundSecret,
|
|
164
|
+
GUARD_OUTBOUND_SECRET_SEVERITY,
|
|
120
165
|
),
|
|
121
166
|
]
|
|
122
167
|
for (const result of checks) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type SecuritySeverity = 'low' | 'medium' | 'high'
|
|
2
|
+
|
|
1
3
|
export const SECURITY_PERMISSIONS = {
|
|
2
4
|
bypassSecretExfilBash: 'security.bypass.secretExfilBash',
|
|
3
5
|
bypassGitExfil: 'security.bypass.gitExfil',
|
|
@@ -7,6 +9,40 @@ export const SECURITY_PERMISSIONS = {
|
|
|
7
9
|
bypassSystemPromptLeak: 'security.bypass.systemPromptLeak',
|
|
8
10
|
bypassOutboundSecret: 'security.bypass.outboundSecret',
|
|
9
11
|
bypassGitRemoteTainted: 'security.bypass.gitRemoteTainted',
|
|
12
|
+
// Severity-tier bypasses. Tiers classify guards on a two-axis policy:
|
|
13
|
+
// high — bypass sends data to a third-party audience outside the
|
|
14
|
+
// operator's control loop (channel readers, remote git host).
|
|
15
|
+
// NO role auto-bypasses; ack required from every role.
|
|
16
|
+
// medium — bypass produces silent attacker-favorable state in model
|
|
17
|
+
// context (env dump, .env contents, IAM creds, secret-shaped
|
|
18
|
+
// session-search hits). Owner bypasses, trusted does not.
|
|
19
|
+
// low — bypass produces a noisy, immediately-recoverable side
|
|
20
|
+
// effect. Owner and trusted bypass. No inhabitants today.
|
|
21
|
+
// Per-guard permissions above continue to work as explicit grants —
|
|
22
|
+
// `tool.before` accepts EITHER the tier OR the per-guard string (OR
|
|
23
|
+
// check). This lets operators knowingly re-open a single high-tier
|
|
24
|
+
// guard for one role without widening the whole tier.
|
|
25
|
+
bypassLow: 'security.bypass.low',
|
|
26
|
+
bypassMedium: 'security.bypass.medium',
|
|
27
|
+
bypassHigh: 'security.bypass.high',
|
|
10
28
|
} as const
|
|
11
29
|
|
|
12
30
|
export type SecurityPermission = (typeof SECURITY_PERMISSIONS)[keyof typeof SECURITY_PERMISSIONS]
|
|
31
|
+
|
|
32
|
+
export const SEVERITY_PERMISSION: Record<SecuritySeverity, string> = {
|
|
33
|
+
low: SECURITY_PERMISSIONS.bypassLow,
|
|
34
|
+
medium: SECURITY_PERMISSIONS.bypassMedium,
|
|
35
|
+
high: SECURITY_PERMISSIONS.bypassHigh,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Per-guard permission strings whose guards are classified `high`. The
|
|
39
|
+
// owner-wildcard expander excludes these so the wildcard sentinel does
|
|
40
|
+
// not auto-grant high-tier bypass to owner. Operators who explicitly
|
|
41
|
+
// want to re-open a high-tier bypass for owner (or any role) can still
|
|
42
|
+
// add the per-guard string to that role's `permissions[]` by hand.
|
|
43
|
+
export const HIGH_TIER_PER_GUARD_PERMISSIONS: readonly string[] = [
|
|
44
|
+
SECURITY_PERMISSIONS.bypassGitExfil,
|
|
45
|
+
SECURITY_PERMISSIONS.bypassGitRemoteTainted,
|
|
46
|
+
SECURITY_PERMISSIONS.bypassOutboundSecret,
|
|
47
|
+
SECURITY_PERMISSIONS.bypassSystemPromptLeak,
|
|
48
|
+
]
|
|
@@ -1,8 +1,28 @@
|
|
|
1
|
+
import type { SecuritySeverity } from '../permissions'
|
|
1
2
|
import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../policy'
|
|
2
3
|
import { getRemoteTaint, recordRemoteTaint } from './remote-taint-state'
|
|
3
4
|
|
|
4
5
|
export const GUARD_GIT_EXFIL = 'gitExfil'
|
|
6
|
+
// Classified `high` (audience-leak axis): `git push` sends every tracked
|
|
7
|
+
// file to a remote git host. The host (GitHub/GitLab/attacker-controlled
|
|
8
|
+
// box) is a third-party audience outside the operator's control loop.
|
|
9
|
+
// Even a private remote owned by an attacker is now outside the
|
|
10
|
+
// perimeter. No role auto-bypasses high — owner pushing from TUI must ack
|
|
11
|
+
// each push. The historical per-guard string `security.bypass.gitExfil`
|
|
12
|
+
// remains valid as an explicit grant for operators who knowingly want to
|
|
13
|
+
// re-open the auto-bypass (see SKILL.md must-not-do guidance).
|
|
14
|
+
export const GUARD_GIT_EXFIL_SEVERITY: SecuritySeverity = 'high'
|
|
5
15
|
export const GUARD_GIT_REMOTE_TAINTED = 'gitRemoteTainted'
|
|
16
|
+
// Classified `high` (audience-leak axis): same path as gitExfil, second
|
|
17
|
+
// step. A push after a mid-session `git remote set-url` to an
|
|
18
|
+
// attacker-controlled URL is exactly the breach pattern that motivated
|
|
19
|
+
// the entire security plugin per PR #134. The recorder-vs-checker split
|
|
20
|
+
// (see comment on recordGitRemoteTaintIfAny below) is still load-bearing:
|
|
21
|
+
// the recorder fires for anyone who can run the underlying command (ack
|
|
22
|
+
// or the per-guard `bypassGitExfil` grant), so even if an operator
|
|
23
|
+
// explicitly grants `bypassGitExfil` to a role, the second-step taint
|
|
24
|
+
// check still fires on the eventual push.
|
|
25
|
+
export const GUARD_GIT_REMOTE_TAINTED_SEVERITY: SecuritySeverity = 'high'
|
|
6
26
|
|
|
7
27
|
// Anchors we reuse: a `git` token must be at start-of-line or follow a shell
|
|
8
28
|
// separator. This blocks `git push` while letting `cgit-something` through
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import type { SecuritySeverity } from '../permissions'
|
|
1
2
|
import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../policy'
|
|
2
3
|
|
|
3
4
|
export const GUARD_OUTBOUND_SECRET = 'outboundSecret'
|
|
5
|
+
// Classified `high` (audience-leak axis): bypass posts credential-shaped
|
|
6
|
+
// text to a chat channel whose readership is a third-party audience
|
|
7
|
+
// outside the operator's control loop. Channel readers, push-notification
|
|
8
|
+
// previews, search indexes, and other bots in the channel all see the
|
|
9
|
+
// secret before the operator can intervene. Owner-in-public-channel is
|
|
10
|
+
// the canonical motivating case: even owner asking the agent to "post the
|
|
11
|
+
// deploy status" should not be able to silently include a stack-trace
|
|
12
|
+
// `Bearer ghp_...` line. The whole point of the high tier is that
|
|
13
|
+
// audience-leak guards require per-call ack from every role, including
|
|
14
|
+
// owner — see AGENTS.md `## Permissions` rules of thumb.
|
|
15
|
+
export const GUARD_OUTBOUND_SECRET_SEVERITY: SecuritySeverity = 'high'
|
|
4
16
|
|
|
5
17
|
const SIGNATURE_PATTERNS: ReadonlyArray<{ kind: string; pattern: RegExp }> = [
|
|
6
18
|
{ kind: 'aws_access_key_id', pattern: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ABIA|ACCA)[A-Z0-9]{16}\b/ },
|
|
@@ -463,10 +463,30 @@ export function detectPromptInjection(prompt: string): InjectionMatch[] {
|
|
|
463
463
|
|
|
464
464
|
const DEFENSE_MARKER = '[security/prompt-injection]'
|
|
465
465
|
|
|
466
|
+
// Subagent prompts are constructed by trusted bundled code, not from raw
|
|
467
|
+
// user input. The backup-diagnose subagent in particular embeds raw git
|
|
468
|
+
// stderr (which legitimately contains literal "git push --help" hint
|
|
469
|
+
// strings on fast-forward rejection or missing-upstream failures) — those
|
|
470
|
+
// hits would otherwise trigger the git_exfil category and inject a "do
|
|
471
|
+
// NOT run git push" rule that contradicts the subagent's own
|
|
472
|
+
// system-prompt instructions to retry with an ack. Under the audience-
|
|
473
|
+
// leak policy the runtime tool.before is the universal backstop for
|
|
474
|
+
// `git push` regardless of role (no role auto-bypasses), so the prompt-
|
|
475
|
+
// side git_exfil category is strictly redundant for subagent origins.
|
|
476
|
+
// Other categories (system_prompt_dump, secret_demand,
|
|
477
|
+
// fake_privileged_skill) still fire for subagents because their threats
|
|
478
|
+
// (e.g. memory-logger ingesting an attacker's transcript) are real.
|
|
479
|
+
function filterForOrigin(matches: InjectionMatch[], origin: SessionPromptEvent['origin']): InjectionMatch[] {
|
|
480
|
+
if (origin?.kind !== 'subagent') return matches
|
|
481
|
+
return matches.filter((m) => m.category !== 'git_exfil')
|
|
482
|
+
}
|
|
483
|
+
|
|
466
484
|
export function applyPromptInjectionDefense(event: SessionPromptEvent): InjectionMatch[] {
|
|
467
|
-
const
|
|
468
|
-
if (
|
|
469
|
-
if (event.prompt.includes(DEFENSE_MARKER)) return
|
|
485
|
+
const allMatches = detectPromptInjection(event.prompt)
|
|
486
|
+
if (allMatches.length === 0) return allMatches
|
|
487
|
+
if (event.prompt.includes(DEFENSE_MARKER)) return allMatches
|
|
488
|
+
const matches = filterForOrigin(allMatches, event.origin)
|
|
489
|
+
if (matches.length === 0) return allMatches
|
|
470
490
|
|
|
471
491
|
const categories = Array.from(new Set(matches.map((m) => m.category))).join(', ')
|
|
472
492
|
const note = [
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import type { SecuritySeverity } from '../permissions'
|
|
1
2
|
import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../policy'
|
|
2
3
|
|
|
3
4
|
export const GUARD_SECRET_EXFIL_BASH = 'secretExfilBash'
|
|
5
|
+
// Classified `medium` (silent-attack axis): bypass dumps the whole
|
|
6
|
+
// environment (every API key, every token) into the agent's tool-result
|
|
7
|
+
// buffer. No direct channel side effect — operator only sees on session
|
|
8
|
+
// review — but the secrets are now in model context and one channel_send
|
|
9
|
+
// away from a third-party audience. Silent at the moment of leak.
|
|
10
|
+
export const GUARD_SECRET_EXFIL_BASH_SEVERITY: SecuritySeverity = 'medium'
|
|
4
11
|
|
|
5
12
|
const DANGEROUS_COMMAND_PATTERNS: ReadonlyArray<{ pattern: RegExp; label: string }> = [
|
|
6
13
|
{ pattern: /(^|[\s;|&(`$])(env|printenv)([\s;|&)`]|$)/, label: 'env / printenv (full environment dump)' },
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
|
|
3
|
+
import type { SecuritySeverity } from '../permissions'
|
|
3
4
|
import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../policy'
|
|
4
5
|
|
|
5
6
|
export const GUARD_SECRET_EXFIL_READ = 'secretExfilRead'
|
|
7
|
+
// Classified `medium` (silent-attack axis): bypass returns `.env` /
|
|
8
|
+
// credential-file contents into model context. Same shape as
|
|
9
|
+
// secretExfilBash — silent at the moment of read, becomes catastrophic on
|
|
10
|
+
// the next channel-side tool call that quotes it.
|
|
11
|
+
export const GUARD_SECRET_EXFIL_READ_SEVERITY: SecuritySeverity = 'medium'
|
|
6
12
|
|
|
7
13
|
const SENSITIVE_BASENAMES = new Set([
|
|
8
14
|
'.env',
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import type { SecuritySeverity } from '../permissions'
|
|
1
2
|
import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../policy'
|
|
2
3
|
|
|
3
4
|
export const GUARD_SESSION_SEARCH_SECRETS = 'sessionSearchSecrets'
|
|
5
|
+
// Classified `medium` (silent-attack axis): bypass returns secret-shaped
|
|
6
|
+
// session-search hits into the agent's tool-result buffer. The operator
|
|
7
|
+
// doesn't see the raw hits — the agent summarizes them — so the leak is
|
|
8
|
+
// silent from the operator's perspective even though it's a read tool.
|
|
9
|
+
// The hits then live in model context as a precondition for a later
|
|
10
|
+
// channel_send leak; outboundSecret would catch the actual send, but
|
|
11
|
+
// silent-recon-then-summarize is its own attack shape.
|
|
12
|
+
export const GUARD_SESSION_SEARCH_SECRETS_SEVERITY: SecuritySeverity = 'medium'
|
|
4
13
|
|
|
5
14
|
const SESSION_SEARCH_TOOLS: ReadonlySet<string> = new Set([
|
|
6
15
|
'session_search',
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import type { SecuritySeverity } from '../permissions'
|
|
1
2
|
import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../policy'
|
|
2
3
|
|
|
3
4
|
export const GUARD_SSRF = 'ssrf'
|
|
5
|
+
// Classified `medium` (silent-attack axis): bypass lets `curl
|
|
6
|
+
// http://169.254.169.254/...` return cloud-metadata IAM credentials into
|
|
7
|
+
// model context. Silent — no channel side effect at the moment of fetch.
|
|
8
|
+
// Catastrophic on follow-up because the model now has live cloud creds.
|
|
9
|
+
export const GUARD_SSRF_SEVERITY: SecuritySeverity = 'medium'
|
|
4
10
|
|
|
5
11
|
const ALWAYS_BLOCKED_HOSTS = new Set([
|
|
6
12
|
'localhost',
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import type { SecuritySeverity } from '../permissions'
|
|
1
2
|
import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../policy'
|
|
2
3
|
|
|
3
4
|
export const GUARD_SYSTEM_PROMPT_LEAK = 'systemPromptLeak'
|
|
5
|
+
// Classified `high` (audience-leak axis): bypass posts TypeClaw runtime
|
|
6
|
+
// fingerprints / system-prompt fragments to a chat. Same shape as
|
|
7
|
+
// outboundSecret — third-party audience, no operator intervention before
|
|
8
|
+
// the leak lands. Disclosure also enables recon for later targeted
|
|
9
|
+
// prompt-injection attacks against this agent.
|
|
10
|
+
export const GUARD_SYSTEM_PROMPT_LEAK_SEVERITY: SecuritySeverity = 'high'
|
|
4
11
|
|
|
5
12
|
const FINGERPRINT_PATTERNS: ReadonlyArray<{ label: string; pattern: RegExp }> = [
|
|
6
13
|
{ label: 'TypeClaw runtime preamble', pattern: /You are a general-purpose AI agent running inside TypeClaw\./ },
|