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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/src/agent/index.ts +37 -4
  3. package/src/agent/restart-handoff/index.ts +91 -0
  4. package/src/agent/restart-handoff/paths.ts +11 -0
  5. package/src/agent/session-origin.ts +30 -10
  6. package/src/agent/subagent-completion-reminder.ts +4 -2
  7. package/src/agent/system-prompt.ts +1 -1
  8. package/src/agent/tools/restart.ts +42 -1
  9. package/src/agent/tools/skip-response.ts +157 -0
  10. package/src/bundled-plugins/memory/README.md +18 -2
  11. package/src/bundled-plugins/memory/index.ts +108 -6
  12. package/src/bundled-plugins/memory/memory-logger.ts +33 -24
  13. package/src/bundled-plugins/security/policies/prompt-injection.ts +1 -1
  14. package/src/channels/adapters/github/auth-app.ts +53 -9
  15. package/src/channels/adapters/github/auth-pat.ts +4 -1
  16. package/src/channels/adapters/github/auth.ts +10 -0
  17. package/src/channels/adapters/github/event-permissions.ts +83 -0
  18. package/src/channels/adapters/github/inbound.ts +126 -1
  19. package/src/channels/adapters/github/index.ts +60 -66
  20. package/src/channels/adapters/github/outbound.ts +65 -17
  21. package/src/channels/adapters/github/permission-guidance.ts +169 -0
  22. package/src/channels/adapters/github/team-membership.ts +56 -0
  23. package/src/channels/router.ts +213 -32
  24. package/src/channels/schema.ts +8 -7
  25. package/src/channels/types.ts +1 -1
  26. package/src/cli/channel.ts +135 -38
  27. package/src/cli/init.ts +133 -86
  28. package/src/cli/inspect-controller.ts +66 -0
  29. package/src/cli/inspect.ts +24 -32
  30. package/src/cli/run.ts +24 -5
  31. package/src/cli/tui.ts +34 -10
  32. package/src/cli/tunnel.ts +453 -14
  33. package/src/config/config.ts +35 -7
  34. package/src/config/providers.ts +64 -56
  35. package/src/init/env-file.ts +66 -0
  36. package/src/init/hatching.ts +32 -5
  37. package/src/init/index.ts +131 -39
  38. package/src/init/validate-api-key.ts +31 -0
  39. package/src/inspect/index.ts +5 -1
  40. package/src/inspect/loop.ts +12 -1
  41. package/src/inspect/replay.ts +15 -1
  42. package/src/run/codex-fetch-observer.ts +377 -0
  43. package/src/run/index.ts +12 -2
  44. package/src/server/index.ts +59 -1
  45. package/src/shared/protocol.ts +1 -1
  46. package/src/skills/typeclaw-channel-github/SKILL.md +45 -1
  47. package/src/skills/typeclaw-tunnels/SKILL.md +33 -1
  48. package/src/tui/index.ts +17 -5
  49. package/src/tunnels/index.ts +1 -0
  50. package/src/tunnels/manager.ts +18 -0
  51. package/src/tunnels/providers/cloudflare-named.ts +224 -0
  52. package/src/tunnels/types.ts +17 -1
  53. 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
+ }