typeclaw 0.10.0 → 0.11.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 +1 -1
- package/src/agent/index.ts +37 -4
- package/src/agent/restart-handoff/index.ts +91 -0
- package/src/agent/restart-handoff/paths.ts +11 -0
- package/src/agent/session-origin.ts +30 -10
- package/src/agent/subagent-completion-reminder.ts +4 -2
- package/src/agent/system-prompt.ts +1 -1
- package/src/agent/tools/restart.ts +42 -1
- package/src/agent/tools/skip-response.ts +157 -0
- package/src/bundled-plugins/memory/README.md +18 -2
- package/src/bundled-plugins/memory/index.ts +108 -6
- package/src/bundled-plugins/memory/memory-logger.ts +33 -24
- package/src/bundled-plugins/security/policies/prompt-injection.ts +1 -1
- package/src/channels/adapters/github/auth-app.ts +53 -9
- package/src/channels/adapters/github/auth-pat.ts +4 -1
- package/src/channels/adapters/github/auth.ts +10 -0
- package/src/channels/adapters/github/event-permissions.ts +83 -0
- package/src/channels/adapters/github/inbound.ts +126 -1
- package/src/channels/adapters/github/index.ts +60 -66
- package/src/channels/adapters/github/outbound.ts +65 -17
- package/src/channels/adapters/github/permission-guidance.ts +169 -0
- package/src/channels/adapters/github/team-membership.ts +56 -0
- package/src/channels/router.ts +213 -32
- package/src/channels/schema.ts +8 -7
- package/src/channels/types.ts +1 -1
- package/src/cli/channel.ts +135 -38
- package/src/cli/init.ts +133 -86
- package/src/cli/inspect-controller.ts +66 -0
- package/src/cli/inspect.ts +24 -32
- package/src/cli/run.ts +24 -5
- package/src/cli/tui.ts +34 -10
- package/src/cli/tunnel.ts +453 -14
- package/src/config/config.ts +35 -7
- package/src/config/providers.ts +64 -56
- package/src/init/env-file.ts +66 -0
- package/src/init/hatching.ts +32 -5
- package/src/init/index.ts +131 -39
- package/src/init/validate-api-key.ts +31 -0
- package/src/inspect/index.ts +5 -1
- package/src/inspect/loop.ts +12 -1
- package/src/inspect/replay.ts +15 -1
- package/src/run/codex-fetch-observer.ts +377 -0
- package/src/run/index.ts +12 -2
- package/src/server/index.ts +59 -1
- package/src/shared/protocol.ts +1 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +45 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +33 -1
- package/src/tui/index.ts +17 -5
- package/src/tunnels/index.ts +1 -0
- package/src/tunnels/manager.ts +18 -0
- package/src/tunnels/providers/cloudflare-named.ts +224 -0
- package/src/tunnels/types.ts +17 -1
- package/typeclaw.schema.json +25 -7
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { PermissionGap } from './event-permissions'
|
|
2
|
+
|
|
3
|
+
export type GithubAuthType = 'pat' | 'app'
|
|
4
|
+
|
|
5
|
+
// What kind of outbound API the adapter was trying to call when it got a
|
|
6
|
+
// permission-failure response. Each value maps to a distinct GitHub App
|
|
7
|
+
// permission family (and, for PATs, a distinct scope), so each surfaces a
|
|
8
|
+
// different remediation message.
|
|
9
|
+
export type OutboundEndpointKind = 'issue-comment' | 'pr-review-reply' | 'discussion-comment'
|
|
10
|
+
|
|
11
|
+
// Parses webhook-register errors of the shape `list hooks failed: <status> <body>`.
|
|
12
|
+
// Returns the status code when it matches the two shapes GitHub emits for
|
|
13
|
+
// missing access on the list-hooks endpoint:
|
|
14
|
+
// - 404 Not Found: the token cannot see the repo at all (private repo
|
|
15
|
+
// gated behind missing repository access — GitHub returns 404 instead of
|
|
16
|
+
// 403 to avoid leaking the existence of private repos).
|
|
17
|
+
// - 403 Forbidden: the token sees the repo but lacks webhook-management
|
|
18
|
+
// permission, OR is blocked by an org SSO/SAML authorization gate.
|
|
19
|
+
// Returns null for any other error (network, malformed slug, create-hook
|
|
20
|
+
// failures, etc.) so the guidance only fires on the actual symptom.
|
|
21
|
+
export function parseListHooksPermissionStatus(error: string): number | null {
|
|
22
|
+
const match = error.match(/^list hooks failed: (404|403)\b/)
|
|
23
|
+
if (match === null) return null
|
|
24
|
+
return Number(match[1])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The labels below intentionally mirror github.com's current UI verbatim so a
|
|
28
|
+
// user can grep their settings page for the exact string. If GitHub renames
|
|
29
|
+
// any of these in a future redesign, update both here and the
|
|
30
|
+
// `permissionGuidance` tests in lifecycle.test.ts.
|
|
31
|
+
//
|
|
32
|
+
// Fine-grained PAT:
|
|
33
|
+
// Settings → Developer settings → Personal access tokens → Fine-grained tokens
|
|
34
|
+
// "Resource owner", "Repository access", "Repository permissions" → "Webhooks" → "Read and write", "Metadata" → "Read-only"
|
|
35
|
+
// GitHub App:
|
|
36
|
+
// Settings → Developer settings → GitHub Apps → <app> → Permissions & events
|
|
37
|
+
// "Repository permissions" → "Webhooks" → "Read and write"
|
|
38
|
+
// Install/configure on the org: <app settings> → Install App / Configure → "Repository access"
|
|
39
|
+
// Classic PAT (legacy, still supported by GitHub but we don't surface it in
|
|
40
|
+
// channel-add prompts):
|
|
41
|
+
// Settings → Developer settings → Personal access tokens (classic)
|
|
42
|
+
// Scope: "admin:repo_hook" (or full "repo" for private repositories)
|
|
43
|
+
export function buildPermissionGuidance(
|
|
44
|
+
authType: GithubAuthType,
|
|
45
|
+
failures: ReadonlyArray<{ repo: string; status: number }>,
|
|
46
|
+
): string {
|
|
47
|
+
const repoList = failures.map((f) => `${f.repo} (${f.status})`).join(', ')
|
|
48
|
+
const lines: string[] = [
|
|
49
|
+
`[github] webhook setup needs more access for: ${repoList}.`,
|
|
50
|
+
' - 404 from GitHub means the token cannot see the repo (GitHub hides private repos behind 404 instead of 403).',
|
|
51
|
+
' - 403 means the token sees the repo but lacks webhook permission, or is blocked by org SAML/SSO.',
|
|
52
|
+
'',
|
|
53
|
+
]
|
|
54
|
+
if (authType === 'pat') {
|
|
55
|
+
lines.push(
|
|
56
|
+
' Fix (fine-grained personal access token):',
|
|
57
|
+
' 1. Open https://github.com/settings/personal-access-tokens and edit the token TypeClaw is using.',
|
|
58
|
+
' 2. Under "Resource owner", select the org that owns the failing repos (e.g. the org in the slug above).',
|
|
59
|
+
' 3. Under "Repository access", choose "Only select repositories" and add every failing repo (or pick "All repositories").',
|
|
60
|
+
' 4. Under "Repository permissions", set "Webhooks" to "Read and write" and "Metadata" to "Read-only".',
|
|
61
|
+
' 5. Save. If the org enforces SAML SSO, click "Configure SSO" next to the token and authorize the org.',
|
|
62
|
+
'',
|
|
63
|
+
' Or (classic personal access token): grant the "admin:repo_hook" scope (or "repo" for private repos),',
|
|
64
|
+
' and on a SAML-protected org click "Authorize" next to the token.',
|
|
65
|
+
)
|
|
66
|
+
} else {
|
|
67
|
+
lines.push(
|
|
68
|
+
' Fix (GitHub App):',
|
|
69
|
+
' 1. Open https://github.com/settings/apps and edit the app TypeClaw is using.',
|
|
70
|
+
' 2. Under "Permissions & events" → "Repository permissions", set "Webhooks" to "Read and write". Save.',
|
|
71
|
+
' 3. From the app page, click "Install App" (or "Configure" if already installed) and select the org that owns the failing repos.',
|
|
72
|
+
' 4. Under "Repository access", choose "Only select repositories" and add every failing repo (or pick "All repositories").',
|
|
73
|
+
' 5. If the app permissions changed in step 2, install owners must accept the updated permissions from the install page before the new access takes effect.',
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
return lines.join('\n')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Always GitHub App: PATs don't have per-installation permission grants
|
|
80
|
+
// (their access is gated by token scopes, surfaced by the existing 404/403
|
|
81
|
+
// flow in webhook-register).
|
|
82
|
+
export function buildAppPermissionPreflightGuidance(gaps: ReadonlyArray<PermissionGap>): string {
|
|
83
|
+
const lines = [
|
|
84
|
+
`[github] GitHub App installation is missing permissions for ${gaps.length} configured event ${gaps.length === 1 ? 'family' : 'families'}:`,
|
|
85
|
+
]
|
|
86
|
+
for (const gap of gaps) {
|
|
87
|
+
const eventList = gap.events.join(', ')
|
|
88
|
+
const grantedLabel = gap.granted === null ? 'none' : gap.granted
|
|
89
|
+
const needLabel = gap.needsWrite ? 'Read and write' : 'Read-only'
|
|
90
|
+
lines.push(` - ${gap.uiLabel}: granted=${grantedLabel}, need=${needLabel} (covers: ${eventList})`)
|
|
91
|
+
}
|
|
92
|
+
lines.push(
|
|
93
|
+
'',
|
|
94
|
+
' Fix:',
|
|
95
|
+
' 1. Open https://github.com/settings/apps and edit the app TypeClaw is using.',
|
|
96
|
+
' 2. Under "Permissions & events" → "Repository permissions", set each missing permission above to the listed level. Save.',
|
|
97
|
+
' 3. Open the install page (Install App / Configure for the org) and accept the updated permissions request — the new access only takes effect after the install owner accepts.',
|
|
98
|
+
' 4. If the org enforces SAML SSO, ensure the App is authorized for the org from the org settings → Third-party Apps page.',
|
|
99
|
+
'',
|
|
100
|
+
' Webhooks already received will continue to deliver, but payload fields and reply attempts that require the missing permission will fail with 403 ("Resource not accessible by integration") until the install is reaccepted.',
|
|
101
|
+
)
|
|
102
|
+
return lines.join('\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The label and (for App auth) the level appear verbatim on github.com so
|
|
106
|
+
// users can grep their settings page for the exact strings. The PAT scope
|
|
107
|
+
// names are GitHub's canonical token-scope identifiers, also verbatim.
|
|
108
|
+
const OUTBOUND_PERMISSION_FOR_KIND: Record<
|
|
109
|
+
OutboundEndpointKind,
|
|
110
|
+
{ label: string; level: 'Read and write'; patScope: string; patFineGrained: string }
|
|
111
|
+
> = {
|
|
112
|
+
'issue-comment': {
|
|
113
|
+
label: 'Issues',
|
|
114
|
+
level: 'Read and write',
|
|
115
|
+
patScope: 'repo (or public_repo for public repos)',
|
|
116
|
+
patFineGrained: 'Issues',
|
|
117
|
+
},
|
|
118
|
+
'pr-review-reply': {
|
|
119
|
+
label: 'Pull requests',
|
|
120
|
+
level: 'Read and write',
|
|
121
|
+
patScope: 'repo',
|
|
122
|
+
patFineGrained: 'Pull requests',
|
|
123
|
+
},
|
|
124
|
+
'discussion-comment': {
|
|
125
|
+
label: 'Discussions',
|
|
126
|
+
level: 'Read and write',
|
|
127
|
+
patScope: 'repo',
|
|
128
|
+
patFineGrained: 'Discussions',
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Decorate an outbound-API failure with the precise github.com permission a
|
|
133
|
+
// user needs to enable. Called only on the 403 + "Resource not accessible by
|
|
134
|
+
// integration" combination — other 403s (org SSO, suspended install) need
|
|
135
|
+
// different remediation and would be mis-described here.
|
|
136
|
+
export function buildOutboundPermissionGuidance(options: {
|
|
137
|
+
authType: GithubAuthType
|
|
138
|
+
endpointKind: OutboundEndpointKind
|
|
139
|
+
}): string {
|
|
140
|
+
const perm = OUTBOUND_PERMISSION_FOR_KIND[options.endpointKind]
|
|
141
|
+
if (options.authType === 'app') {
|
|
142
|
+
return [
|
|
143
|
+
'',
|
|
144
|
+
` Fix (GitHub App): the App needs "${perm.label}" → "${perm.level}".`,
|
|
145
|
+
' 1. Open https://github.com/settings/apps and edit the app TypeClaw is using.',
|
|
146
|
+
` 2. Under "Permissions & events" → "Repository permissions", set "${perm.label}" to "${perm.level}". Save.`,
|
|
147
|
+
' 3. Open the install page (Install App / Configure for the org) and accept the updated permissions request — the new access only takes effect after the install owner accepts.',
|
|
148
|
+
].join('\n')
|
|
149
|
+
}
|
|
150
|
+
return [
|
|
151
|
+
'',
|
|
152
|
+
` Fix (fine-grained personal access token): grant "${perm.patFineGrained}" → "Read and write" on the failing repo.`,
|
|
153
|
+
' 1. Open https://github.com/settings/personal-access-tokens and edit the token TypeClaw is using.',
|
|
154
|
+
` 2. Under "Repository permissions", set "${perm.patFineGrained}" to "Read and write". Save.`,
|
|
155
|
+
` 3. If the org enforces SAML SSO, click "Configure SSO" next to the token and authorize the org.`,
|
|
156
|
+
'',
|
|
157
|
+
` Or (classic personal access token): grant the "${perm.patScope}" scope; SAML-protected orgs additionally need "Authorize" next to the token.`,
|
|
158
|
+
].join('\n')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 403 with this exact body is GitHub's signal that the call would succeed
|
|
162
|
+
// with the right permissions. Other 403 bodies (e.g. "OAuth token… needs to
|
|
163
|
+
// be authorized for this organization", suspended installation) need
|
|
164
|
+
// different remediation, so the decoration matcher is intentionally narrow.
|
|
165
|
+
const INTEGRATION_PERMISSION_DENIAL = 'Resource not accessible by integration'
|
|
166
|
+
|
|
167
|
+
export function isOutboundPermissionDenial(status: number, body: string): boolean {
|
|
168
|
+
return status === 403 && body.includes(INTEGRATION_PERMISSION_DENIAL)
|
|
169
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Best-effort "is the bot a member of this team?" lookup, used to gate
|
|
2
|
+
// `pull_request.review_requested` inbounds when GitHub assigns a *team*
|
|
3
|
+
// rather than a user as the reviewer.
|
|
4
|
+
//
|
|
5
|
+
// Cached per (org, slug, login) for the adapter's lifetime: team membership
|
|
6
|
+
// changes rarely, and a stale cache costs us one missed review (the next
|
|
7
|
+
// request rebuilds it). Errors fall closed (return false): we'd rather drop
|
|
8
|
+
// a real review request than wake the agent on a team the bot isn't in.
|
|
9
|
+
|
|
10
|
+
const ACTIVE_MEMBERSHIP_STATE = 'active'
|
|
11
|
+
|
|
12
|
+
export type TeamMembershipChecker = (input: { org: string; slug: string; login: string }) => Promise<boolean>
|
|
13
|
+
|
|
14
|
+
export function createTeamMembershipChecker(options: {
|
|
15
|
+
token: () => Promise<string>
|
|
16
|
+
fetchImpl?: typeof fetch
|
|
17
|
+
}): TeamMembershipChecker {
|
|
18
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
19
|
+
const cache = new Map<string, boolean>()
|
|
20
|
+
|
|
21
|
+
return async ({ org, slug, login }) => {
|
|
22
|
+
const key = `${org}/${slug}#${login}`
|
|
23
|
+
const cached = cache.get(key)
|
|
24
|
+
if (cached !== undefined) return cached
|
|
25
|
+
|
|
26
|
+
const result = await lookup(fetchImpl, await options.token(), org, slug, login)
|
|
27
|
+
cache.set(key, result)
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function lookup(
|
|
33
|
+
fetchImpl: typeof fetch,
|
|
34
|
+
token: string,
|
|
35
|
+
org: string,
|
|
36
|
+
slug: string,
|
|
37
|
+
login: string,
|
|
38
|
+
): Promise<boolean> {
|
|
39
|
+
const url = `https://api.github.com/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/memberships/${encodeURIComponent(login)}`
|
|
40
|
+
let res: Response
|
|
41
|
+
try {
|
|
42
|
+
res = await fetchImpl(url, {
|
|
43
|
+
headers: {
|
|
44
|
+
accept: 'application/vnd.github+json',
|
|
45
|
+
authorization: `Bearer ${token}`,
|
|
46
|
+
'x-github-api-version': '2022-11-28',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
if (res.status === 404) return false
|
|
53
|
+
if (!res.ok) return false
|
|
54
|
+
const body = (await res.json().catch(() => null)) as { state?: unknown } | null
|
|
55
|
+
return typeof body?.state === 'string' && body.state === ACTIVE_MEMBERSHIP_STATE
|
|
56
|
+
}
|