typeclaw 0.9.1 → 0.10.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/package.json +2 -2
- package/scripts/require-parallel.ts +41 -15
- package/src/agent/index.ts +9 -7
- package/src/agent/live-subagents.ts +0 -1
- package/src/agent/session-origin.ts +10 -0
- package/src/agent/subagent-completion-reminder.ts +4 -1
- package/src/agent/system-prompt.ts +5 -5
- package/src/agent/tools/restart.ts +13 -2
- package/src/agent/tools/spawn-subagent.ts +0 -1
- package/src/agent/tools/subagent-output.ts +3 -51
- package/src/bundled-plugins/memory/dreaming-state.ts +51 -2
- package/src/bundled-plugins/memory/index.ts +55 -25
- package/src/bundled-plugins/memory/memory-retrieval.ts +1 -1
- package/src/bundled-plugins/memory/migration.ts +21 -17
- package/src/bundled-plugins/memory/stream-io.ts +71 -1
- package/src/bundled-plugins/security/index.ts +19 -17
- package/src/bundled-plugins/security/permissions.ts +9 -8
- package/src/bundled-plugins/security/policies/cron-promotion.ts +26 -9
- package/src/bundled-plugins/security/policies/git-exfil.ts +23 -15
- package/src/bundled-plugins/security/policies/role-promotion.ts +25 -18
- package/src/channels/manager.ts +7 -0
- package/src/channels/router.ts +267 -14
- package/src/channels/schema.ts +22 -1
- package/src/cli/compose.ts +23 -2
- package/src/cli/cron.ts +1 -1
- package/src/cli/inspect.ts +105 -12
- package/src/cli/logs.ts +17 -2
- package/src/cli/role.ts +2 -2
- package/src/compose/logs.ts +8 -4
- package/src/config/config.ts +8 -0
- package/src/config/providers.ts +18 -0
- package/src/container/index.ts +1 -1
- package/src/container/logs.ts +38 -11
- package/src/cron/bridge.ts +25 -4
- package/src/hostd/daemon.ts +44 -24
- package/src/hostd/portbroker-manager.ts +19 -3
- package/src/init/dockerfile.ts +199 -4
- package/src/init/gitignore.ts +8 -0
- package/src/inspect/index.ts +42 -5
- package/src/inspect/live.ts +32 -1
- package/src/inspect/loop.ts +20 -0
- package/src/inspect/render.ts +32 -0
- package/src/inspect/replay.ts +14 -0
- package/src/inspect/types.ts +26 -0
- package/src/permissions/builtins.ts +29 -21
- package/src/permissions/permissions.ts +32 -5
- package/src/role-claim/code.ts +9 -9
- package/src/role-claim/controller.ts +3 -2
- package/src/role-claim/match-rule.ts +14 -19
- package/src/role-claim/pending.ts +2 -2
- package/src/run/index.ts +1 -0
- package/src/server/index.ts +59 -19
- package/src/shared/protocol.ts +30 -0
- package/src/skills/typeclaw-codex-cli/SKILL.md +324 -0
- package/src/skills/typeclaw-codex-cli/references/auth-flow.md +144 -0
- package/src/skills/typeclaw-codex-cli/references/stop-hook.md +92 -0
- package/src/skills/typeclaw-codex-cli/references/tmux-driving.md +239 -0
- package/src/skills/typeclaw-config/SKILL.md +39 -32
- package/src/skills/typeclaw-config/references/recommended-mounts.md +233 -0
- package/src/skills/typeclaw-permissions/SKILL.md +24 -18
- package/src/test-helpers/wait-for.ts +15 -7
- package/typeclaw.schema.json +111 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, appendFile, readdir, writeFile, rename } from 'node:fs/promises'
|
|
1
|
+
import { readFile, appendFile, readdir, stat, writeFile, rename } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { getDreamedIds, loadDreamingState } from './dreaming-state'
|
|
@@ -8,7 +8,59 @@ import { parseEventLine, type StreamEvent } from './stream-events'
|
|
|
8
8
|
const STREAM_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/
|
|
9
9
|
const STREAM_DATE_FROM_FILENAME = /^(\d{4}-\d{2}-\d{2})\.jsonl$/
|
|
10
10
|
|
|
11
|
+
// Per-file event cache. `(mtimeMs, ctimeMs, size)` is the invalidation key,
|
|
12
|
+
// mirroring `load-shards.ts`'s shard cache. The three writers in this module
|
|
13
|
+
// — `appendEvents` (memory-logger appends), `writeEventsAtomic` (dreaming
|
|
14
|
+
// compaction + migration), and any external `writeFile` — all bump mtime
|
|
15
|
+
// and/or ctime, so stat-based invalidation is sufficient without explicit
|
|
16
|
+
// hooks. ctimeMs guards metadata-preserving external edits (rsync -t,
|
|
17
|
+
// `touch -r`, restored backups, `git checkout` with timestamps): the kernel
|
|
18
|
+
// always bumps ctime on inode content changes and ctime cannot be backdated
|
|
19
|
+
// via utimes.
|
|
20
|
+
//
|
|
21
|
+
// Module-level keyed by absolute file path. One Bun process owns one agent
|
|
22
|
+
// dir in production (the container stage), so cardinality is small. Multi-
|
|
23
|
+
// path support exists because dreaming compacts multiple files per run and
|
|
24
|
+
// memory_search reads every dated stream.
|
|
25
|
+
type StreamFileCacheEntry = {
|
|
26
|
+
mtimeMs: number
|
|
27
|
+
ctimeMs: number
|
|
28
|
+
size: number
|
|
29
|
+
events: StreamEvent[]
|
|
30
|
+
}
|
|
31
|
+
const streamFileCache = new Map<string, StreamFileCacheEntry>()
|
|
32
|
+
|
|
11
33
|
export async function readEvents(path: string): Promise<StreamEvent[]> {
|
|
34
|
+
const fileStat = await statFile(path)
|
|
35
|
+
if (fileStat === null) {
|
|
36
|
+
// File disappeared since last cache populate (e.g. dreaming dropped a
|
|
37
|
+
// fully-GC'd day). Drop the entry so a future recreate gets fresh
|
|
38
|
+
// content.
|
|
39
|
+
streamFileCache.delete(path)
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cached = streamFileCache.get(path)
|
|
44
|
+
if (
|
|
45
|
+
cached !== undefined &&
|
|
46
|
+
cached.mtimeMs === fileStat.mtimeMs &&
|
|
47
|
+
cached.ctimeMs === fileStat.ctimeMs &&
|
|
48
|
+
cached.size === fileStat.size
|
|
49
|
+
) {
|
|
50
|
+
return cached.events
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const events = await readEventsFromDisk(path)
|
|
54
|
+
streamFileCache.set(path, {
|
|
55
|
+
mtimeMs: fileStat.mtimeMs,
|
|
56
|
+
ctimeMs: fileStat.ctimeMs,
|
|
57
|
+
size: fileStat.size,
|
|
58
|
+
events,
|
|
59
|
+
})
|
|
60
|
+
return events
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readEventsFromDisk(path: string): Promise<StreamEvent[]> {
|
|
12
64
|
let raw: string
|
|
13
65
|
try {
|
|
14
66
|
raw = await readFile(path, 'utf-8')
|
|
@@ -34,6 +86,24 @@ export async function readEvents(path: string): Promise<StreamEvent[]> {
|
|
|
34
86
|
return events
|
|
35
87
|
}
|
|
36
88
|
|
|
89
|
+
async function statFile(path: string): Promise<{ mtimeMs: number; ctimeMs: number; size: number } | null> {
|
|
90
|
+
try {
|
|
91
|
+
const s = await stat(path)
|
|
92
|
+
return { mtimeMs: s.mtimeMs, ctimeMs: s.ctimeMs, size: s.size }
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null
|
|
95
|
+
throw err
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Test-only helper. Clears the in-memory stream-file cache so tests that
|
|
100
|
+
// exercise the cache invalidation path can simulate a cold start without
|
|
101
|
+
// spinning up a fresh process. Mirrors `__resetShardCacheForTests` in
|
|
102
|
+
// `load-shards.ts`.
|
|
103
|
+
export function __resetStreamFileCacheForTests(): void {
|
|
104
|
+
streamFileCache.clear()
|
|
105
|
+
}
|
|
106
|
+
|
|
37
107
|
export async function appendEvents(path: string, events: readonly StreamEvent[]): Promise<void> {
|
|
38
108
|
if (events.length === 0) return
|
|
39
109
|
const joined = events.map((e) => `${JSON.stringify(e)}\n`).join('')
|
|
@@ -49,22 +49,23 @@ type PerGuardSecurityPermission = Exclude<
|
|
|
49
49
|
// not a silent fallback.
|
|
50
50
|
const BYPASS_ROLE_HINT = {
|
|
51
51
|
[SECURITY_PERMISSIONS.bypassSecretExfilBash]:
|
|
52
|
-
'
|
|
52
|
+
'owner and trusted have it by default (medium tier); member and guest do not. Operators can grant `security.bypass.secretExfilBash` explicitly in roles.<role>.permissions[] to widen.',
|
|
53
53
|
[SECURITY_PERMISSIONS.bypassGitExfil]:
|
|
54
|
-
'
|
|
54
|
+
'owner and trusted have it by default (medium tier); member and guest do not. The audience-leak surface for git lives in `gitRemoteTainted` (high tier, owner-only) — pushing to an attacker-retargeted remote is still blocked for trusted by the two-step taint defense.',
|
|
55
55
|
[SECURITY_PERMISSIONS.bypassGitRemoteTainted]:
|
|
56
|
-
'
|
|
57
|
-
[SECURITY_PERMISSIONS.bypassSecretExfilRead]:
|
|
58
|
-
|
|
59
|
-
[SECURITY_PERMISSIONS.
|
|
60
|
-
[SECURITY_PERMISSIONS.
|
|
61
|
-
'
|
|
56
|
+
'only owner has it by default (high tier). The two-step taint defense (recorder + checker) still fires whenever the actor lacks `security.bypass.gitRemoteTainted`, including across owner-granted gitExfil bypasses.',
|
|
57
|
+
[SECURITY_PERMISSIONS.bypassSecretExfilRead]:
|
|
58
|
+
'owner and trusted have it by default (medium tier); member and guest do not.',
|
|
59
|
+
[SECURITY_PERMISSIONS.bypassSsrf]: 'owner and trusted have it by default (medium tier); member and guest do not.',
|
|
60
|
+
[SECURITY_PERMISSIONS.bypassSessionSearchSecrets]:
|
|
61
|
+
'owner and trusted have it by default (medium tier); member and guest do not.',
|
|
62
|
+
[SECURITY_PERMISSIONS.bypassSystemPromptLeak]: 'only owner has it by default (high tier).',
|
|
62
63
|
[SECURITY_PERMISSIONS.bypassOutboundSecret]:
|
|
63
|
-
'
|
|
64
|
+
'only owner has it by default (high tier). The audience-leak risk: an owner-permissioned channel author can silently include credentials in outbound messages. Operators who match owner to a channel author should narrow that match or remove owner from `roles.owner.permissions[]` for those origins.',
|
|
64
65
|
[SECURITY_PERMISSIONS.bypassRolePromotion]:
|
|
65
|
-
'
|
|
66
|
+
'owner and trusted have it by default (medium tier); member and guest do not. The privilege-escalation defense for trusted now depends on operator review of `typeclaw.json` backup commits — `roles` is restart-required, so the operator has wall-clock time to revert before the new role table takes effect. Operators who do not review can re-tighten by replacing `roles.trusted.permissions[]` with an explicit list that omits `security.bypass.medium`.',
|
|
66
67
|
[SECURITY_PERMISSIONS.bypassCronPromotion]:
|
|
67
|
-
'
|
|
68
|
+
'owner and trusted have it by default (medium tier); member and guest do not. Same shape as rolePromotion but deferred: a new cron job (or a changed scheduledByRole) fires at schedule-time as the stamped role. The operator-review window between write and execution is the trusted-tier defense.',
|
|
68
69
|
} as const satisfies Record<PerGuardSecurityPermission, string>
|
|
69
70
|
|
|
70
71
|
function withPermissionHint(
|
|
@@ -83,12 +84,13 @@ function withPermissionHint(
|
|
|
83
84
|
|
|
84
85
|
export default definePlugin({
|
|
85
86
|
permissions: Object.values(SECURITY_PERMISSIONS),
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
87
|
+
// No wildcard exclusions: owner bypasses every security tier by default
|
|
88
|
+
// under the role-tower model. `BUILTIN_ROLES.owner.permissions` carries
|
|
89
|
+
// `security.bypass.{low,medium,high}` explicitly; the wildcard sentinel
|
|
90
|
+
// additionally fans out to every per-guard string (including high-tier
|
|
91
|
+
// ones). The owner-in-public-channel defense now lives in
|
|
92
|
+
// `roles.owner.match[]` discipline, not in the language defaults.
|
|
93
|
+
ownerWildcardExclusions: [],
|
|
92
94
|
plugin: async (ctx) => ({
|
|
93
95
|
hooks: {
|
|
94
96
|
'session.prompt': async (event) => {
|
|
@@ -37,16 +37,17 @@ export const SEVERITY_PERMISSION: Record<SecuritySeverity, string> = {
|
|
|
37
37
|
high: SECURITY_PERMISSIONS.bypassHigh,
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// Per-guard permission strings whose guards are classified `high`.
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
40
|
+
// Per-guard permission strings whose guards are classified `high`.
|
|
41
|
+
// Plumbed through to the owner-wildcard expander's `ownerWildcardExclusions`
|
|
42
|
+
// parameter at boot; the bundled security plugin currently passes `[]` so
|
|
43
|
+
// owner DOES auto-bypass every high-tier per-guard string, but third-party
|
|
44
|
+
// plugins (or a future tightening of the bundled defaults) can use this
|
|
45
|
+
// constant to exclude high-tier strings from the wildcard expansion.
|
|
46
|
+
// Keep this list in sync with the `'high'` classifications in
|
|
47
|
+
// `policies/*.ts` — the drift-guard test in `permissions.test.ts` will
|
|
48
|
+
// fail if a guard's severity constant disagrees with its membership here.
|
|
45
49
|
export const HIGH_TIER_PER_GUARD_PERMISSIONS: readonly string[] = [
|
|
46
|
-
SECURITY_PERMISSIONS.bypassGitExfil,
|
|
47
50
|
SECURITY_PERMISSIONS.bypassGitRemoteTainted,
|
|
48
51
|
SECURITY_PERMISSIONS.bypassOutboundSecret,
|
|
49
52
|
SECURITY_PERMISSIONS.bypassSystemPromptLeak,
|
|
50
|
-
SECURITY_PERMISSIONS.bypassRolePromotion,
|
|
51
|
-
SECURITY_PERMISSIONS.bypassCronPromotion,
|
|
52
53
|
]
|
|
@@ -7,8 +7,23 @@ import type { SecuritySeverity } from '../permissions'
|
|
|
7
7
|
import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../policy'
|
|
8
8
|
|
|
9
9
|
export const GUARD_CRON_PROMOTION = 'cronPromotion'
|
|
10
|
-
// Classified `
|
|
11
|
-
// `rolePromotion
|
|
10
|
+
// Classified `medium` (silent-attack axis). Originally `high`; reclassified
|
|
11
|
+
// for the same reason as `rolePromotion`: the deferred-execution surface
|
|
12
|
+
// is still operator-reviewable before the job fires. `cron.json` is
|
|
13
|
+
// force-committed by the auto-backup plugin, the change appears in
|
|
14
|
+
// `git log` and backup commits, and the cron consumer dispatches by
|
|
15
|
+
// schedule — there is wall-clock time between the privileged write and
|
|
16
|
+
// the privileged execution during which the operator can revert or
|
|
17
|
+
// disable. Bypass produces operator-reviewable state, not direct
|
|
18
|
+
// audience-leak.
|
|
19
|
+
//
|
|
20
|
+
// Net effect on the role-tower model: owner and trusted both bypass
|
|
21
|
+
// without ack; member and guest still get blocked. The defense for
|
|
22
|
+
// trusted depends on backup-commit review discipline — same tradeoff
|
|
23
|
+
// as `rolePromotion`. Operators who want to keep this at high for
|
|
24
|
+
// trusted can subtract: replace `roles.trusted.permissions[]` with an
|
|
25
|
+
// explicit list that omits `security.bypass.medium`, then add narrower
|
|
26
|
+
// per-guard medium grants as needed.
|
|
12
27
|
//
|
|
13
28
|
// Cron is the deferred-execution sibling of `roles`. Every cron job
|
|
14
29
|
// carries a `scheduledByRole` field that the runtime stamps into the
|
|
@@ -17,12 +32,14 @@ export const GUARD_CRON_PROMOTION = 'cronPromotion'
|
|
|
17
32
|
// table"). The `parseCronFile` boot gate rejects entries without
|
|
18
33
|
// `scheduledByRole`, but it accepts any role name the file declares.
|
|
19
34
|
//
|
|
20
|
-
// Concrete breach pattern
|
|
21
|
-
// `cron.json` authors a brand-new job with
|
|
22
|
-
// and a prompt that does whatever the
|
|
23
|
-
// running as owner. The cron consumer
|
|
24
|
-
// session resolves to `owner` because
|
|
25
|
-
// table. The agent has laundered
|
|
35
|
+
// Concrete breach pattern blocked at `medium`: a `member`-role agent
|
|
36
|
+
// that can `write` `cron.json` authors a brand-new job with
|
|
37
|
+
// `"scheduledByRole": "owner"` and a prompt that does whatever the
|
|
38
|
+
// agent's tool surface allows when running as owner. The cron consumer
|
|
39
|
+
// fires it on schedule; the firing session resolves to `owner` because
|
|
40
|
+
// that role name exists in the role table. The agent has laundered
|
|
41
|
+
// itself into owner via the schedule. This guard blocks the first step
|
|
42
|
+
// — member does not carry `bypass.medium`.
|
|
26
43
|
//
|
|
27
44
|
// Same two-step shape as `gitRemoteTainted`: "do a privileged write
|
|
28
45
|
// now, run the privileged thing later." This guard blocks the first
|
|
@@ -66,7 +83,7 @@ export const GUARD_CRON_PROMOTION = 'cronPromotion'
|
|
|
66
83
|
// is treated as new and flagged. The only false positive is "operator
|
|
67
84
|
// authored a fresh `cron.json` with privileged jobs," which they
|
|
68
85
|
// acknowledge in the same call.
|
|
69
|
-
export const GUARD_CRON_PROMOTION_SEVERITY: SecuritySeverity = '
|
|
86
|
+
export const GUARD_CRON_PROMOTION_SEVERITY: SecuritySeverity = 'medium'
|
|
70
87
|
|
|
71
88
|
export type CronPromotionFinding =
|
|
72
89
|
| { kind: 'job-added'; id: string; scheduledByRole: string }
|
|
@@ -3,24 +3,32 @@ import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../
|
|
|
3
3
|
import { getRemoteTaint, recordRemoteTaint } from './remote-taint-state'
|
|
4
4
|
|
|
5
5
|
export const GUARD_GIT_EXFIL = 'gitExfil'
|
|
6
|
-
// Classified `
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
6
|
+
// Classified `medium` (silent-attack axis). Originally `high`; reclassified
|
|
7
|
+
// because the actual audience-leak surface for `git push` lives in
|
|
8
|
+
// `gitRemoteTainted`, not here. A `git push` to a CLEAN, operator-configured
|
|
9
|
+
// remote is not audience-leak — the audience (the remote git host) was
|
|
10
|
+
// chosen by the operator and is inside their perimeter. The breach pattern
|
|
11
|
+
// PR #134 was written for (re-point origin to attacker URL, then push) is
|
|
12
|
+
// gated by `gitRemoteTainted` (still high). The recorder-vs-checker split
|
|
13
|
+
// is what makes this reclassification safe: the recorder fires for any
|
|
14
|
+
// actor who can run a `git remote set-url` (per-guard bypass OR the
|
|
15
|
+
// medium-tier permission via the OR check), so trusted's first-step
|
|
16
|
+
// set-url still records taint and the second-step push still gets caught
|
|
17
|
+
// by `gitRemoteTainted` even though trusted no longer needs to ack the
|
|
18
|
+
// push itself. Net effect: trusted users can push to remotes the operator
|
|
19
|
+
// configured without per-call acks, but cannot retarget-and-push.
|
|
20
|
+
export const GUARD_GIT_EXFIL_SEVERITY: SecuritySeverity = 'medium'
|
|
15
21
|
export const GUARD_GIT_REMOTE_TAINTED = 'gitRemoteTainted'
|
|
16
|
-
// Classified `high` (audience-leak axis):
|
|
17
|
-
//
|
|
22
|
+
// Classified `high` (audience-leak axis): the actual audience-leak gate
|
|
23
|
+
// for git. A push after a mid-session `git remote set-url` to an
|
|
18
24
|
// attacker-controlled URL is exactly the breach pattern that motivated
|
|
19
|
-
// the entire security plugin per PR #134.
|
|
25
|
+
// the entire security plugin per PR #134. Stays high regardless of how
|
|
26
|
+
// `gitExfil` is classified — the two are independent per-guard strings
|
|
27
|
+
// AND independent tier classifications. The recorder-vs-checker split
|
|
20
28
|
// (see comment on recordGitRemoteTaintIfAny below) is still load-bearing:
|
|
21
|
-
// the recorder fires for anyone who can run the underlying
|
|
22
|
-
//
|
|
23
|
-
//
|
|
29
|
+
// the recorder fires for anyone who can run the underlying `set-url`
|
|
30
|
+
// command (ack, per-guard `bypassGitExfil`, OR the medium-tier permission
|
|
31
|
+
// — which now includes trusted by default), so the second-step taint
|
|
24
32
|
// check still fires on the eventual push.
|
|
25
33
|
export const GUARD_GIT_REMOTE_TAINTED_SEVERITY: SecuritySeverity = 'high'
|
|
26
34
|
|
|
@@ -8,25 +8,32 @@ import type { SecuritySeverity } from '../permissions'
|
|
|
8
8
|
import { ACKNOWLEDGE_GUARDS, type SecurityBlock, isGuardAcknowledged } from '../policy'
|
|
9
9
|
|
|
10
10
|
export const GUARD_ROLE_PROMOTION = 'rolePromotion'
|
|
11
|
-
// Classified `
|
|
11
|
+
// Classified `medium` (silent-attack axis). Originally `high`; reclassified
|
|
12
|
+
// because the privilege escalation does NOT take effect until the operator
|
|
13
|
+
// reloads or restarts — `roles` is `restart-required` in FIELD_EFFECTS, and
|
|
14
|
+
// even the `match`-only path that's classified `applied` writes through
|
|
15
|
+
// `typeclaw.json` which is force-committed by the auto-backup plugin on
|
|
16
|
+
// idle. The operator sees the file change in `git log`, in `typeclaw reload`
|
|
17
|
+
// output, and in their backup commits BEFORE the new role mapping takes
|
|
18
|
+
// effect. There is an operator-visible step between bypass and breach,
|
|
19
|
+
// which puts this guard squarely on the medium axis: bypass produces
|
|
20
|
+
// attacker-favorable state in operator-reviewable surface, not direct
|
|
21
|
+
// audience-leak.
|
|
12
22
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
23
|
+
// Net effect on the role-tower model: owner and trusted both bypass without
|
|
24
|
+
// ack; member and guest still get blocked. The defense for trusted now
|
|
25
|
+
// depends on operator config-review discipline — if backup commits are
|
|
26
|
+
// reviewed and `typeclaw reload` output is read before applying, a
|
|
27
|
+
// trusted-laundered role promotion is caught before it fires. Operators
|
|
28
|
+
// who do not review can re-tighten by adding `security.bypass.rolePromotion`
|
|
29
|
+
// to `trusted.permissions[]` as an explicit subtraction (replace the
|
|
30
|
+
// default tier grant with a narrower list) — see typeclaw-permissions skill.
|
|
21
31
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// generalizes from "post credentials" to "promote the asker". No role
|
|
28
|
-
// auto-bypasses; per-call ack or an explicit `security.bypass.rolePromotion`
|
|
29
|
-
// grant is required.
|
|
32
|
+
// Breach pattern blocked at `medium`: a `member`-role speaker in a chat
|
|
33
|
+
// asks "promote me to admin"; the agent edits typeclaw.json; the change is
|
|
34
|
+
// schema-valid, managedConfig accepts it, nonWorkspaceWrite allowlists
|
|
35
|
+
// typeclaw.json — but this guard still blocks because member does not
|
|
36
|
+
// carry `bypass.medium` by default.
|
|
30
37
|
//
|
|
31
38
|
// What counts as a promotion (any of):
|
|
32
39
|
// 1. A role's `permissions[]` gained an entry.
|
|
@@ -58,7 +65,7 @@ export const GUARD_ROLE_PROMOTION = 'rolePromotion'
|
|
|
58
65
|
// agent cannot start a claim, only consume one whose code the
|
|
59
66
|
// operator already broadcast. That makes the bypass intentionally
|
|
60
67
|
// out-of-band — do not extend this guard to cover it.
|
|
61
|
-
export const GUARD_ROLE_PROMOTION_SEVERITY: SecuritySeverity = '
|
|
68
|
+
export const GUARD_ROLE_PROMOTION_SEVERITY: SecuritySeverity = 'medium'
|
|
62
69
|
|
|
63
70
|
export type RolePromotionFinding = {
|
|
64
71
|
role: string
|
package/src/channels/manager.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { PermissionService } from '@/permissions'
|
|
|
5
5
|
import type { GithubSecretsBlock } from '@/secrets'
|
|
6
6
|
import { SecretsKakaoCredentialStore } from '@/secrets/kakao-store'
|
|
7
7
|
import { SecretsBackend } from '@/secrets/storage'
|
|
8
|
+
import type { Stream } from '@/stream'
|
|
8
9
|
|
|
9
10
|
import { createDiscordBotAdapter, type DiscordBotAdapter } from './adapters/discord-bot'
|
|
10
11
|
import { createGithubAdapter, type GithubAdapter } from './adapters/github'
|
|
@@ -78,6 +79,11 @@ export type ChannelManagerOptions = {
|
|
|
78
79
|
// a URL" so error logs can be precise. Same shape as
|
|
79
80
|
// `tunnelUrlForChannel` for consistency. Optional for tests.
|
|
80
81
|
tunnelConfiguredForChannel?: (channelName: string) => boolean
|
|
82
|
+
// Forwarded to the router as `stream`. When set, every inbound the
|
|
83
|
+
// router sees is published as a tagged broadcast for inspect surfacing.
|
|
84
|
+
// Production wiring (`src/run/index.ts`) always passes the agent's
|
|
85
|
+
// Stream; tests typically omit it.
|
|
86
|
+
stream?: Stream
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
export type ChannelManager = {
|
|
@@ -113,6 +119,7 @@ export function createChannelManager(options: ChannelManagerOptions): ChannelMan
|
|
|
113
119
|
...(options.createSessionForChannel ? { createSessionForChannel: options.createSessionForChannel } : {}),
|
|
114
120
|
...(options.permissions ? { permissions: options.permissions } : {}),
|
|
115
121
|
...(options.claimHandler ? { claimHandler: options.claimHandler } : {}),
|
|
122
|
+
...(options.stream ? { stream: options.stream } : {}),
|
|
116
123
|
})
|
|
117
124
|
const createDiscordAdapter = options.createDiscordAdapter ?? createDiscordBotAdapter
|
|
118
125
|
const createGithub = options.createGithubAdapter ?? createGithubAdapter
|