typeclaw 0.36.7 → 0.37.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 +2 -2
- package/package.json +3 -2
- package/src/agent/index.ts +31 -11
- package/src/agent/live-sessions.ts +12 -0
- package/src/agent/model-fallback.ts +17 -15
- package/src/agent/model-overrides.ts +2 -2
- package/src/agent/session-meta.ts +10 -0
- package/src/agent/subagents.ts +11 -2
- package/src/agent/system-prompt.ts +9 -3
- package/src/agent/todo/continuation-policy.ts +6 -3
- package/src/agent/todo/continuation-wiring.ts +4 -2
- package/src/agent/todo/continuation.ts +3 -3
- package/src/agent/tools/todo/index.ts +27 -4
- package/src/bundled-plugins/agent-browser/index.ts +33 -108
- package/src/bundled-plugins/agent-browser/shim.ts +3 -94
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
- package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
- package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
- package/src/bundled-plugins/memory/README.md +80 -23
- package/src/bundled-plugins/memory/append-tool.ts +74 -53
- package/src/bundled-plugins/memory/citation-superset.ts +4 -0
- package/src/bundled-plugins/memory/citations.ts +54 -0
- package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
- package/src/bundled-plugins/memory/dreaming.ts +444 -21
- package/src/bundled-plugins/memory/index.ts +544 -400
- package/src/bundled-plugins/memory/load-memory.ts +87 -10
- package/src/bundled-plugins/memory/load-shards.ts +48 -22
- package/src/bundled-plugins/memory/memory-logger.ts +95 -106
- package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
- package/src/bundled-plugins/memory/parent-link.ts +33 -0
- package/src/bundled-plugins/memory/paths.ts +12 -0
- package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
- package/src/bundled-plugins/memory/references/load-references.ts +212 -0
- package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
- package/src/bundled-plugins/memory/search-tool.ts +282 -45
- package/src/bundled-plugins/memory/stream-events.ts +1 -0
- package/src/bundled-plugins/memory/stream-io.ts +28 -3
- package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
- package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
- package/src/bundled-plugins/memory/vector/config.ts +28 -0
- package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
- package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
- package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
- package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
- package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
- package/src/bundled-plugins/memory/vector/passages.ts +125 -0
- package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
- package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
- package/src/bundled-plugins/memory/vector/startup.ts +71 -0
- package/src/bundled-plugins/memory/vector/store.ts +203 -0
- package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
- package/src/channels/router.ts +239 -40
- package/src/cli/incomplete-init.ts +57 -0
- package/src/cli/init.ts +143 -12
- package/src/cli/inspect.ts +11 -5
- package/src/cli/model.ts +112 -34
- package/src/cli/restart.ts +24 -0
- package/src/cli/start.ts +24 -0
- package/src/cli/tunnel.ts +53 -8
- package/src/config/config.ts +110 -19
- package/src/config/index.ts +5 -1
- package/src/config/models-mutation.ts +29 -11
- package/src/config/providers-mutation.ts +2 -2
- package/src/config/providers.ts +146 -12
- package/src/container/shared.ts +9 -0
- package/src/container/start.ts +87 -4
- package/src/cron/consumer.ts +13 -7
- package/src/hostd/models.ts +64 -0
- package/src/hostd/paths.ts +6 -0
- package/src/hostd/portbroker-manager.ts +2 -2
- package/src/init/checkpoint.ts +201 -0
- package/src/init/dockerfile.ts +164 -51
- package/src/init/gitignore.ts +7 -7
- package/src/init/index.ts +41 -9
- package/src/init/line-auth.ts +50 -21
- package/src/init/models-dev.ts +96 -21
- package/src/init/oauth-login.ts +3 -3
- package/src/init/progress.ts +29 -0
- package/src/init/validate-api-key.ts +4 -0
- package/src/inspect/index.ts +13 -6
- package/src/inspect/item-list.ts +11 -2
- package/src/inspect/live-list.ts +65 -0
- package/src/inspect/open-item.ts +22 -1
- package/src/inspect/session-list.ts +29 -0
- package/src/models/embedding-model.ts +114 -0
- package/src/models/transformers-version.ts +55 -0
- package/src/plugin/types.ts +3 -0
- package/src/portbroker/container-server.ts +23 -0
- package/src/portbroker/forward-request-bus.ts +35 -0
- package/src/portbroker/forward-result-bus.ts +2 -3
- package/src/portbroker/hostd-client.ts +182 -36
- package/src/portbroker/index.ts +6 -1
- package/src/portbroker/protocol.ts +9 -2
- package/src/run/channel-session-factory.ts +11 -1
- package/src/run/index.ts +41 -7
- package/src/server/command-runner.ts +24 -1
- package/src/server/index.ts +42 -8
- package/src/shared/index.ts +2 -0
- package/src/shared/protocol.ts +31 -0
- package/src/skills/typeclaw-channels/SKILL.md +4 -4
- package/src/skills/typeclaw-config/SKILL.md +2 -2
- package/src/skills/typeclaw-memory/SKILL.md +3 -1
- package/src/skills/typeclaw-permissions/SKILL.md +3 -3
- package/src/skills/typeclaw-skills/SKILL.md +1 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
- package/src/tunnels/providers/cloudflare-quick.ts +65 -7
- package/src/tunnels/upstream-probe.ts +25 -0
- package/typeclaw.schema.json +156 -67
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
- package/src/portbroker/bind-with-forward.ts +0 -102
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
KNOWN_PROVIDERS,
|
|
6
|
+
listKnownModelRefs,
|
|
7
|
+
listKnownProviderVendorIds,
|
|
8
|
+
providerIdsForVendor,
|
|
9
|
+
type KnownProviderId,
|
|
10
|
+
type KnownProviderVendorId,
|
|
11
|
+
type ModelRef,
|
|
12
|
+
} from '@/config/providers'
|
|
13
|
+
|
|
14
|
+
// In-folder scratch for an in-progress `typeclaw init`, co-located with the
|
|
15
|
+
// agent it describes. Lives under the agent's `.typeclaw/` (the same gitignored
|
|
16
|
+
// local-scratch dir as the persistent-$HOME overlay), so it self-cleans when
|
|
17
|
+
// the half-init folder is deleted, survives a folder rename mid-init, and is
|
|
18
|
+
// inspectable right next to the thing being resumed. Gitignored via
|
|
19
|
+
// `TRULY_IGNORED_PATTERNS`; never committed.
|
|
20
|
+
export const INIT_CHECKPOINT_PATH = join('.typeclaw', 'init-progress.json')
|
|
21
|
+
|
|
22
|
+
export const WIZARD_CHECKPOINT_VERSION = 1
|
|
23
|
+
|
|
24
|
+
export type WizardChannelChoice = 'slack' | 'discord' | 'telegram' | 'kakaotalk' | 'github' | 'none'
|
|
25
|
+
|
|
26
|
+
export type AuthMethod = 'api-key' | 'oauth'
|
|
27
|
+
|
|
28
|
+
// Only stable selection IDs — never `llmAuth`, tokens, OAuth data, channel
|
|
29
|
+
// secrets, the volatile models.dev catalog, or full `ModelOption` objects. The
|
|
30
|
+
// projection in `checkpointFromSelections` is the single seam that enforces
|
|
31
|
+
// this, so a secret can never leak into host state by accident.
|
|
32
|
+
export interface WizardAnswerCheckpointV1 {
|
|
33
|
+
version: typeof WIZARD_CHECKPOINT_VERSION
|
|
34
|
+
cwd: string
|
|
35
|
+
updatedAt: string
|
|
36
|
+
vendorId?: KnownProviderVendorId
|
|
37
|
+
providerId?: KnownProviderId
|
|
38
|
+
modelRef?: ModelRef | string
|
|
39
|
+
authMethod?: AuthMethod
|
|
40
|
+
visionVendorId?: KnownProviderVendorId
|
|
41
|
+
visionProviderId?: KnownProviderId
|
|
42
|
+
visionModelRef?: ModelRef | string
|
|
43
|
+
visionAuthMethod?: AuthMethod
|
|
44
|
+
channelChoice?: WizardChannelChoice
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WizardCheckpointStore {
|
|
48
|
+
load(cwd: string): Promise<WizardAnswerCheckpointV1 | undefined>
|
|
49
|
+
save(cwd: string, checkpoint: WizardAnswerCheckpointV1): Promise<void>
|
|
50
|
+
clear(cwd: string): Promise<void>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Selections the wizard already holds, projected to the persisted shape. Keep
|
|
54
|
+
// this the ONLY place that reads `WizardState` so secret fields are physically
|
|
55
|
+
// unable to reach the checkpoint file.
|
|
56
|
+
export interface WizardCheckpointSelections {
|
|
57
|
+
cwd: string
|
|
58
|
+
vendorId?: KnownProviderVendorId
|
|
59
|
+
providerId?: KnownProviderId
|
|
60
|
+
modelRef?: ModelRef | string
|
|
61
|
+
authMethod?: AuthMethod
|
|
62
|
+
visionVendorId?: KnownProviderVendorId
|
|
63
|
+
visionProviderId?: KnownProviderId
|
|
64
|
+
visionModelRef?: ModelRef | string
|
|
65
|
+
visionAuthMethod?: AuthMethod
|
|
66
|
+
channelChoice?: WizardChannelChoice
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function checkpointFromSelections(selections: WizardCheckpointSelections): WizardAnswerCheckpointV1 {
|
|
70
|
+
return {
|
|
71
|
+
version: WIZARD_CHECKPOINT_VERSION,
|
|
72
|
+
cwd: selections.cwd,
|
|
73
|
+
updatedAt: new Date().toISOString(),
|
|
74
|
+
...(selections.vendorId !== undefined ? { vendorId: selections.vendorId } : {}),
|
|
75
|
+
...(selections.providerId !== undefined ? { providerId: selections.providerId } : {}),
|
|
76
|
+
...(selections.modelRef !== undefined ? { modelRef: selections.modelRef } : {}),
|
|
77
|
+
...(selections.authMethod !== undefined ? { authMethod: selections.authMethod } : {}),
|
|
78
|
+
...(selections.visionVendorId !== undefined ? { visionVendorId: selections.visionVendorId } : {}),
|
|
79
|
+
...(selections.visionProviderId !== undefined ? { visionProviderId: selections.visionProviderId } : {}),
|
|
80
|
+
...(selections.visionModelRef !== undefined ? { visionModelRef: selections.visionModelRef } : {}),
|
|
81
|
+
...(selections.visionAuthMethod !== undefined ? { visionAuthMethod: selections.visionAuthMethod } : {}),
|
|
82
|
+
...(selections.channelChoice !== undefined ? { channelChoice: selections.channelChoice } : {}),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Drop any saved field that no longer references a real vendor/provider/model.
|
|
87
|
+
// Stale values cascade downward: an unknown provider invalidates its model and
|
|
88
|
+
// auth-method too, since those were chosen for a provider that no longer
|
|
89
|
+
// exists. Returns a sanitized copy; never throws on drift.
|
|
90
|
+
export function sanitizeCheckpointAgainstCatalog(
|
|
91
|
+
checkpoint: WizardAnswerCheckpointV1,
|
|
92
|
+
validModelRefs: ReadonlySet<string> = new Set(listKnownModelRefs()),
|
|
93
|
+
): WizardAnswerCheckpointV1 {
|
|
94
|
+
const sanitized: WizardAnswerCheckpointV1 = {
|
|
95
|
+
version: WIZARD_CHECKPOINT_VERSION,
|
|
96
|
+
cwd: checkpoint.cwd,
|
|
97
|
+
updatedAt: checkpoint.updatedAt,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const vendor = pruneVendor(checkpoint.vendorId)
|
|
101
|
+
const provider = pruneProvider(vendor, checkpoint.providerId)
|
|
102
|
+
if (vendor !== undefined) sanitized.vendorId = vendor
|
|
103
|
+
if (provider !== undefined) {
|
|
104
|
+
sanitized.providerId = provider
|
|
105
|
+
if (checkpoint.modelRef !== undefined && validModelRefs.has(checkpoint.modelRef)) {
|
|
106
|
+
sanitized.modelRef = checkpoint.modelRef
|
|
107
|
+
}
|
|
108
|
+
if (checkpoint.authMethod !== undefined) sanitized.authMethod = checkpoint.authMethod
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const visionVendor = pruneVendor(checkpoint.visionVendorId)
|
|
112
|
+
const visionProvider = pruneProvider(visionVendor, checkpoint.visionProviderId)
|
|
113
|
+
if (visionVendor !== undefined) sanitized.visionVendorId = visionVendor
|
|
114
|
+
if (visionProvider !== undefined) {
|
|
115
|
+
sanitized.visionProviderId = visionProvider
|
|
116
|
+
if (checkpoint.visionModelRef !== undefined && validModelRefs.has(checkpoint.visionModelRef)) {
|
|
117
|
+
sanitized.visionModelRef = checkpoint.visionModelRef
|
|
118
|
+
}
|
|
119
|
+
if (checkpoint.visionAuthMethod !== undefined) sanitized.visionAuthMethod = checkpoint.visionAuthMethod
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (checkpoint.channelChoice !== undefined) sanitized.channelChoice = checkpoint.channelChoice
|
|
123
|
+
|
|
124
|
+
return sanitized
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function pruneVendor(vendorId: KnownProviderVendorId | undefined): KnownProviderVendorId | undefined {
|
|
128
|
+
if (vendorId === undefined) return undefined
|
|
129
|
+
return listKnownProviderVendorIds().includes(vendorId) ? vendorId : undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pruneProvider(
|
|
133
|
+
vendorId: KnownProviderVendorId | undefined,
|
|
134
|
+
providerId: KnownProviderId | undefined,
|
|
135
|
+
): KnownProviderId | undefined {
|
|
136
|
+
if (vendorId === undefined || providerId === undefined) return undefined
|
|
137
|
+
if (!(providerId in KNOWN_PROVIDERS)) return undefined
|
|
138
|
+
return providerIdsForVendor(vendorId).includes(providerId) ? providerId : undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function checkpointFilePath(cwd: string): string {
|
|
142
|
+
return join(cwd, INIT_CHECKPOINT_PATH)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createLocalWizardCheckpointStore(): WizardCheckpointStore {
|
|
146
|
+
return {
|
|
147
|
+
async load(cwd) {
|
|
148
|
+
const path = checkpointFilePath(cwd)
|
|
149
|
+
let parsed: unknown
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(await readFile(path, 'utf8'))
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
if (!isValidCheckpoint(parsed)) return undefined
|
|
156
|
+
return parsed
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Atomic write: temp + rename within the agent's .typeclaw/ so a crash
|
|
160
|
+
// mid-write never leaves a half-written file that `load` would misparse and
|
|
161
|
+
// discard.
|
|
162
|
+
async save(cwd, checkpoint) {
|
|
163
|
+
const final = checkpointFilePath(cwd)
|
|
164
|
+
const tmp = `${final}.${process.pid}.tmp`
|
|
165
|
+
await mkdir(dirname(final), { recursive: true })
|
|
166
|
+
await writeFile(tmp, JSON.stringify(checkpoint), { mode: 0o600 })
|
|
167
|
+
await rename(tmp, final)
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
async clear(cwd) {
|
|
171
|
+
try {
|
|
172
|
+
await unlink(checkpointFilePath(cwd))
|
|
173
|
+
} catch {}
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isValidCheckpoint(value: unknown): value is WizardAnswerCheckpointV1 {
|
|
179
|
+
if (typeof value !== 'object' || value === null) return false
|
|
180
|
+
const v = value as Record<string, unknown>
|
|
181
|
+
if (v.version !== WIZARD_CHECKPOINT_VERSION) return false
|
|
182
|
+
if (typeof v.cwd !== 'string') return false
|
|
183
|
+
if (typeof v.updatedAt !== 'string') return false
|
|
184
|
+
// Every optional selection field must be a string when present. Membership
|
|
185
|
+
// (is this a real provider/model?) is the sanitizer's job, but a non-string
|
|
186
|
+
// here is structurally corrupt and would later index KNOWN_PROVIDERS or join
|
|
187
|
+
// into prompt text with a wrong shape — reject it at load.
|
|
188
|
+
return OPTIONAL_STRING_FIELDS.every((field) => v[field] === undefined || typeof v[field] === 'string')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const OPTIONAL_STRING_FIELDS = [
|
|
192
|
+
'vendorId',
|
|
193
|
+
'providerId',
|
|
194
|
+
'modelRef',
|
|
195
|
+
'authMethod',
|
|
196
|
+
'visionVendorId',
|
|
197
|
+
'visionProviderId',
|
|
198
|
+
'visionModelRef',
|
|
199
|
+
'visionAuthMethod',
|
|
200
|
+
'channelChoice',
|
|
201
|
+
] as const satisfies ReadonlyArray<keyof WizardAnswerCheckpointV1>
|
package/src/init/dockerfile.ts
CHANGED
|
@@ -18,6 +18,10 @@ export type BuildDockerfileOptions = {
|
|
|
18
18
|
// host signal — e.g. a bare `buildDockerfile()` in tests — stays small and
|
|
19
19
|
// deterministic.
|
|
20
20
|
cjkFontsAuto?: boolean
|
|
21
|
+
// Emit a BuildKit Dockerfile (default) or strip BuildKit-only directives for
|
|
22
|
+
// hosts without buildx. `start` sets this to false when buildx is absent so
|
|
23
|
+
// the legacy `docker build` can still build the agent image.
|
|
24
|
+
buildKit?: boolean
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
// Apt packages that EVERY image must have — git for the agent runtime,
|
|
@@ -75,6 +79,7 @@ const BASELINE_APT_PACKAGES = [
|
|
|
75
79
|
'util-linux',
|
|
76
80
|
'bubblewrap',
|
|
77
81
|
'jq',
|
|
82
|
+
'libgomp1',
|
|
78
83
|
] as const
|
|
79
84
|
|
|
80
85
|
// curl-impersonate is the only currently-working way to query DuckDuckGo from
|
|
@@ -293,9 +298,20 @@ set -eu
|
|
|
293
298
|
# (missing library, port conflict, malformed args). Without the
|
|
294
299
|
# explicit liveness probe below, the shim would then export DISPLAY
|
|
295
300
|
# and exec bun, agent-browser launches would die with "cannot open
|
|
296
|
-
# display", and the operator would chase a phantom bug.
|
|
297
|
-
#
|
|
298
|
-
#
|
|
301
|
+
# display", and the operator would chase a phantom bug. A monitor
|
|
302
|
+
# subshell owns Xvfb, \`wait\`s for it, and drops a status file the
|
|
303
|
+
# instant it exits; the poll loop checks that file (before the socket
|
|
304
|
+
# check) so an early exit becomes a clear stderr line and a non-zero
|
|
305
|
+
# shim exit.
|
|
306
|
+
#
|
|
307
|
+
# We do NOT probe liveness with \`kill -0 "\$xvfb_pid"\`. A backgrounded
|
|
308
|
+
# child that exits before the shell \`wait\`s for it becomes a zombie,
|
|
309
|
+
# and \`kill -0\` returns success on a zombie PID (it still exists in
|
|
310
|
+
# the process table). Under load the shell reaps the zombie lazily, so
|
|
311
|
+
# \`kill -0\` reported the dead Xvfb as alive for up to the full 3s
|
|
312
|
+
# window — the loop then timed out and printed the misleading "did not
|
|
313
|
+
# create socket within 3s" diagnostic instead of "exited immediately".
|
|
314
|
+
# The status-file handshake sidesteps zombie semantics entirely.
|
|
299
315
|
#
|
|
300
316
|
# We DO NOT use \`xvfb-run\`. xvfb-run hangs forever when it runs as
|
|
301
317
|
# PID 1 inside a container: its SIGUSR1-based ready handshake races
|
|
@@ -451,30 +467,45 @@ start_xvfb() {
|
|
|
451
467
|
if ! command -v Xvfb >/dev/null 2>&1; then
|
|
452
468
|
return 0
|
|
453
469
|
fi
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
470
|
+
# A monitor subshell owns Xvfb and \`wait\`s for it (the bare command
|
|
471
|
+
# blocks until exit), then writes Xvfb's exit code to a status file.
|
|
472
|
+
# The poll loop below reads that file instead of probing \`kill -0\` —
|
|
473
|
+
# see invariant 2 above for why zombie semantics make \`kill -0\`
|
|
474
|
+
# unreliable. \`set +e\` inside the subshell keeps the outer \`set -e\`
|
|
475
|
+
# from killing the monitor before it records a non-zero Xvfb exit.
|
|
476
|
+
xvfb_status="/tmp/typeclaw-xvfb-status.$$"
|
|
477
|
+
rm -f "$xvfb_status"
|
|
478
|
+
(
|
|
479
|
+
set +e
|
|
480
|
+
setpriv --bounding-set -net_admin --inh-caps -net_admin --ambient-caps -net_admin \\
|
|
481
|
+
-- Xvfb :99 -screen 0 1920x1080x24 -ac +extension RANDR -nolisten tcp \\
|
|
482
|
+
>/dev/null 2>&1
|
|
483
|
+
printf '%s\\n' "$?" > "$xvfb_status"
|
|
484
|
+
) &
|
|
457
485
|
xvfb_pid=$!
|
|
458
486
|
export DISPLAY=:99
|
|
459
|
-
# Poll
|
|
460
|
-
#
|
|
461
|
-
#
|
|
462
|
-
#
|
|
463
|
-
# as a
|
|
464
|
-
# "cannot open display" downstream.
|
|
487
|
+
# Poll every 10ms up to ~3s. Xvfb cold start is typically ~20-50ms on
|
|
488
|
+
# a modern host; 3s covers slow Docker Desktop VMs, Rosetta/QEMU
|
|
489
|
+
# emulation, and loaded CI runners. The status-file check comes FIRST
|
|
490
|
+
# so an Xvfb that creates the socket and then immediately dies is still
|
|
491
|
+
# treated as a startup failure.
|
|
465
492
|
i=0
|
|
466
493
|
while [ $i -lt 300 ]; do
|
|
467
|
-
if [ -
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
fi
|
|
471
|
-
if ! kill -0 "$xvfb_pid" 2>/dev/null; then
|
|
494
|
+
if [ -f "$xvfb_status" ]; then
|
|
495
|
+
wait "$xvfb_pid" 2>/dev/null || true
|
|
496
|
+
rm -f "$xvfb_status"
|
|
472
497
|
echo "typeclaw-entrypoint: Xvfb exited immediately; cannot start headed display (docker.file.xvfb=true)" >&2
|
|
473
498
|
exit 1
|
|
474
499
|
fi
|
|
500
|
+
if [ -S /tmp/.X11-unix/X99 ]; then
|
|
501
|
+
rm -f "$xvfb_status"
|
|
502
|
+
unset i xvfb_pid xvfb_status
|
|
503
|
+
return 0
|
|
504
|
+
fi
|
|
475
505
|
sleep 0.01
|
|
476
506
|
i=$((i + 1))
|
|
477
507
|
done
|
|
508
|
+
rm -f "$xvfb_status"
|
|
478
509
|
echo "typeclaw-entrypoint: Xvfb did not create /tmp/.X11-unix/X99 within 3s; refusing to continue (docker.file.xvfb=true)" >&2
|
|
479
510
|
exit 1
|
|
480
511
|
}
|
|
@@ -547,6 +578,72 @@ RUN echo "${encoded}" | base64 -d > ${TYPECLAW_ENTRYPOINT_PATH} \\
|
|
|
547
578
|
&& chmod +x ${TYPECLAW_ENTRYPOINT_PATH}`
|
|
548
579
|
}
|
|
549
580
|
|
|
581
|
+
// Layer 7: install @huggingface/transformers with its linux-native binaries.
|
|
582
|
+
//
|
|
583
|
+
// The host node_modules is bind-mounted over /agent at runtime carrying
|
|
584
|
+
// whatever binaries the host (typically macOS) resolved, so this layer seeds
|
|
585
|
+
// LINUX binaries into an image path that survives the bind mount and still
|
|
586
|
+
// wins Node/Bun resolution. Two native-dep mechanisms, fixed differently:
|
|
587
|
+
//
|
|
588
|
+
// 1. onnxruntime-node ships its addon via a POSTINSTALL that materializes the
|
|
589
|
+
// binary inside the package dir, so a plain `bun add` covers it.
|
|
590
|
+
// 2. sharp resolves its native code from SEPARATE `@img/sharp-*` optional
|
|
591
|
+
// PACKAGES, not a postinstall addon. sharp is a MANDATORY dep of
|
|
592
|
+
// transformers, loaded eagerly because the package main chains
|
|
593
|
+
// transformers.js -> utils/image.js (top-level `import sharp`) even though
|
|
594
|
+
// typeclaw only does text feature-extraction. `bun add
|
|
595
|
+
// @huggingface/transformers` alone does NOT pull the linux `@img/*` when
|
|
596
|
+
// the bind-mounted host tree already satisfies sharp with the macOS ones —
|
|
597
|
+
// so `sharp.js` throws at import ("Could not load the sharp module using
|
|
598
|
+
// the linux-<arch> runtime") and the container exits at startup. We
|
|
599
|
+
// install the arch-matched linux platform packages EXPLICITLY to fix it.
|
|
600
|
+
//
|
|
601
|
+
// CRITICAL — install location. `typeclaw start` bind-mounts the host agent
|
|
602
|
+
// folder over /agent at runtime (`-v <cwd>:/agent`), so anything written under
|
|
603
|
+
// /agent/node_modules at BUILD time is MASKED at runtime and can never win.
|
|
604
|
+
// The prior fix ran this `bun add` under `WORKDIR /agent`, populating
|
|
605
|
+
// /agent/node_modules — invisible behind the bind mount, so the sharp crash
|
|
606
|
+
// persisted. We install from `WORKDIR /` instead: the linux packages land in
|
|
607
|
+
// the IMAGE's /node_modules, which is NOT masked by the /agent mount. Node/Bun
|
|
608
|
+
// resolution for `require('@img/sharp-linux-<arch>')` ascends from
|
|
609
|
+
// /agent/node_modules/sharp up the directory chain and finds /node_modules,
|
|
610
|
+
// so the linux binary resolves. WORKDIR is restored to /agent afterwards so
|
|
611
|
+
// later layers and the runtime CWD are unchanged.
|
|
612
|
+
//
|
|
613
|
+
// EXACT transformers version this image installs. Must stay in lockstep with
|
|
614
|
+
// `@huggingface/transformers` in the repo package.json (a guard test in
|
|
615
|
+
// dockerfile.test.ts asserts the two agree). The pin is EXACT — not a caret —
|
|
616
|
+
// because this layer's `bun add` runs at `WORKDIR /` with NO project lockfile,
|
|
617
|
+
// so the repo `bun.lock` does NOT constrain it. A bare `@huggingface/
|
|
618
|
+
// transformers` (no version) would resolve npm `latest` at BUILD time: the day
|
|
619
|
+
// a newer transformers ships, the container would install it while the
|
|
620
|
+
// sharp/libvips pins below stay fixed, and the mismatched sharp platform
|
|
621
|
+
// packages crash at container import ("Could not load the sharp module using
|
|
622
|
+
// the linux-<arch> runtime"). Pinning the version closes that drift.
|
|
623
|
+
export const TRANSFORMERS_VERSION = '4.2.0'
|
|
624
|
+
|
|
625
|
+
// sharp + libvips versions are pinned to what @huggingface/transformers@4.2.0
|
|
626
|
+
// resolves. A future transformers bump that moves sharp must bump
|
|
627
|
+
// TRANSFORMERS_VERSION, `sharp@`, `@img/sharp-linux-*`, and
|
|
628
|
+
// `@img/sharp-libvips-linux-*` together — mismatched sharp/libvips platform
|
|
629
|
+
// packages are a known failure mode. `$TARGETARCH` is `arm64` or `amd64`; an
|
|
630
|
+
// empty value (bare `docker build` without buildx) falls back to x64 for
|
|
631
|
+
// determinism.
|
|
632
|
+
const LAYER_TRANSFORMERS_INSTALL = `# Layer 7: install @huggingface/transformers with its linux-native binaries.
|
|
633
|
+
# Installs the linux onnxruntime-node addon AND sharp's linux platform packages
|
|
634
|
+
# (@img/sharp-linux-*) into the image's /node_modules so they survive the
|
|
635
|
+
# runtime /agent bind mount and still win Node/Bun resolution from
|
|
636
|
+
# /agent/node_modules/sharp. The transformers version is pinned EXACT (this
|
|
637
|
+
# bun add has no lockfile) — see src/init/dockerfile.ts for the rationale.
|
|
638
|
+
WORKDIR /
|
|
639
|
+
RUN SHARP_ARCH="$(if [ "\${TARGETARCH:-amd64}" = "arm64" ]; then echo arm64; else echo x64; fi)" \\
|
|
640
|
+
&& bun add \\
|
|
641
|
+
@huggingface/transformers@${TRANSFORMERS_VERSION} \\
|
|
642
|
+
sharp@0.34.5 \\
|
|
643
|
+
"@img/sharp-linux-\${SHARP_ARCH}@0.34.5" \\
|
|
644
|
+
"@img/sharp-libvips-linux-\${SHARP_ARCH}@1.2.4"
|
|
645
|
+
WORKDIR /agent`
|
|
646
|
+
|
|
550
647
|
// Claude Code's official installer is `curl | bash`, not apt — can't live
|
|
551
648
|
// in APT_FEATURES. Layer placed after the toggle apt install (so curl + ca-
|
|
552
649
|
// certificates from the baseline are guaranteed present) and before the
|
|
@@ -1033,7 +1130,7 @@ const TYPECLAW_CX_GLOBAL_HOOKS = JSON.stringify({
|
|
|
1033
1130
|
},
|
|
1034
1131
|
})
|
|
1035
1132
|
|
|
1036
|
-
function renderCodexCliInstallLayer(enabled: boolean): string {
|
|
1133
|
+
function renderCodexCliInstallLayer(enabled: boolean, buildKit: boolean): string {
|
|
1037
1134
|
if (!enabled) return ''
|
|
1038
1135
|
return `# Layer 5.7 (toggle): install OpenAI's Codex CLI. Opt-in via
|
|
1039
1136
|
# typeclaw.json#docker.file.codexCli. The skill \`typeclaw-codex-cli\`
|
|
@@ -1043,8 +1140,7 @@ function renderCodexCliInstallLayer(enabled: boolean): string {
|
|
|
1043
1140
|
# are pre-written at build time so the operator subagent never has to
|
|
1044
1141
|
# construct that JSON itself — same load-bearing reason as the Claude
|
|
1045
1142
|
# Code layer above.
|
|
1046
|
-
RUN
|
|
1047
|
-
bun install -g @openai/codex \\
|
|
1143
|
+
RUN ${bunCacheMount(buildKit)}bun install -g @openai/codex \\
|
|
1048
1144
|
&& codex --version > /dev/null \\
|
|
1049
1145
|
&& cat > ${TYPECLAW_CX_SESSION_START_HOOK_PATH} <<'TYPECLAW_CX_SESSION_START_HOOK_EOF'
|
|
1050
1146
|
${TYPECLAW_CX_SESSION_START_HOOK_SCRIPT}TYPECLAW_CX_SESSION_START_HOOK_EOF
|
|
@@ -1129,15 +1225,19 @@ export function buildDockerfile(
|
|
|
1129
1225
|
config: DockerfileConfig = defaultConfig(),
|
|
1130
1226
|
options: BuildDockerfileOptions = {},
|
|
1131
1227
|
): string {
|
|
1228
|
+
// buildKit is the default; `start` passes false on hosts without buildx so the
|
|
1229
|
+
// generated Dockerfile omits the `# syntax=` pragma and every cache mount —
|
|
1230
|
+
// the legacy builder accepts the result as-is, no text post-processing needed.
|
|
1231
|
+
const buildKit = options.buildKit !== false
|
|
1132
1232
|
const cjkFonts = resolveCjkFonts(config.cjkFonts, options.cjkFontsAuto ?? false)
|
|
1133
1233
|
const toggleAptArgs = collectToggleAptArgs(config, cjkFonts)
|
|
1134
|
-
const ghKeyringLayer = renderGhKeyringLayer(config.gh)
|
|
1234
|
+
const ghKeyringLayer = renderGhKeyringLayer(config.gh, buildKit)
|
|
1135
1235
|
const cloudflaredLayer = renderCloudflaredLayer(config.cloudflared)
|
|
1136
1236
|
const customLines = renderCustomDockerfileLines(config.append)
|
|
1137
1237
|
const baseImageVersion = options.baseImageVersion ?? null
|
|
1138
1238
|
|
|
1139
1239
|
const claudeCodeLayer = renderClaudeCodeInstallLayer(config.claudeCode)
|
|
1140
|
-
const codexCliLayer = renderCodexCliInstallLayer(config.codexCli)
|
|
1240
|
+
const codexCliLayer = renderCodexCliInstallLayer(config.codexCli, buildKit)
|
|
1141
1241
|
const fromAndHeavyLayers =
|
|
1142
1242
|
baseImageVersion !== null
|
|
1143
1243
|
? renderVersionedHead(
|
|
@@ -1147,11 +1247,12 @@ export function buildDockerfile(
|
|
|
1147
1247
|
cloudflaredLayer,
|
|
1148
1248
|
claudeCodeLayer,
|
|
1149
1249
|
codexCliLayer,
|
|
1250
|
+
buildKit,
|
|
1150
1251
|
)
|
|
1151
|
-
: renderInlineHead(ghKeyringLayer, toggleAptArgs, cloudflaredLayer, claudeCodeLayer, codexCliLayer)
|
|
1252
|
+
: renderInlineHead(ghKeyringLayer, toggleAptArgs, cloudflaredLayer, claudeCodeLayer, codexCliLayer, buildKit)
|
|
1152
1253
|
|
|
1153
|
-
|
|
1154
|
-
# AUTOGENERATED by typeclaw — do not edit.
|
|
1254
|
+
const header = buildKit ? `${BUILDKIT_HEADER}\n` : ''
|
|
1255
|
+
return `${header}# AUTOGENERATED by typeclaw — do not edit.
|
|
1155
1256
|
# This file is rewritten on every \`typeclaw start\` from src/init/dockerfile.ts
|
|
1156
1257
|
# in the typeclaw repo. Local edits will be overwritten (and committed away if
|
|
1157
1258
|
# the working tree is dirty). To change the template, edit dockerfile.ts there.
|
|
@@ -1197,8 +1298,9 @@ function renderVersionedHead(
|
|
|
1197
1298
|
cloudflaredLayer: string,
|
|
1198
1299
|
claudeCodeLayer: string,
|
|
1199
1300
|
codexCliLayer: string,
|
|
1301
|
+
buildKit: boolean,
|
|
1200
1302
|
): string {
|
|
1201
|
-
const toggleAptLayer = toggleAptArgs.length === 0 ? '' : `${renderToggleAptInstallLayer(toggleAptArgs)}\n\n`
|
|
1303
|
+
const toggleAptLayer = toggleAptArgs.length === 0 ? '' : `${renderToggleAptInstallLayer(toggleAptArgs, buildKit)}\n\n`
|
|
1202
1304
|
const cloudflaredBlock = cloudflaredLayer === '' ? '' : `${cloudflaredLayer}\n\n`
|
|
1203
1305
|
const claudeCodeBlock = claudeCodeLayer === '' ? '' : `${claudeCodeLayer}\n\n`
|
|
1204
1306
|
const codexCliBlock = codexCliLayer === '' ? '' : `${codexCliLayer}\n\n`
|
|
@@ -1210,6 +1312,8 @@ ARG TARGETARCH
|
|
|
1210
1312
|
|
|
1211
1313
|
${ghKeyringLayer}${toggleAptLayer}${cloudflaredBlock}${claudeCodeBlock}${codexCliBlock}${renderEntrypointShimLayer()}
|
|
1212
1314
|
|
|
1315
|
+
${LAYER_TRANSFORMERS_INSTALL}
|
|
1316
|
+
|
|
1213
1317
|
`
|
|
1214
1318
|
}
|
|
1215
1319
|
|
|
@@ -1223,6 +1327,7 @@ function renderInlineHead(
|
|
|
1223
1327
|
cloudflaredLayer: string,
|
|
1224
1328
|
claudeCodeLayer: string,
|
|
1225
1329
|
codexCliLayer: string,
|
|
1330
|
+
buildKit: boolean,
|
|
1226
1331
|
): string {
|
|
1227
1332
|
const baselineAndToggleArgs = [...BASELINE_APT_PACKAGES, ...toggleAptArgs]
|
|
1228
1333
|
const cloudflaredBlock = cloudflaredLayer === '' ? '' : `${cloudflaredLayer}\n\n`
|
|
@@ -1248,9 +1353,7 @@ ${ghKeyringLayer}# Layer 2 (changes when the package list changes): the actual a
|
|
|
1248
1353
|
#
|
|
1249
1354
|
# No \`rm -rf /var/lib/apt/lists/*\` because the lists live on a cache mount
|
|
1250
1355
|
# that is excluded from the image layer by definition.
|
|
1251
|
-
RUN
|
|
1252
|
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
1253
|
-
apt-get update \\
|
|
1356
|
+
RUN ${aptCacheMount(buildKit)}apt-get update \\
|
|
1254
1357
|
&& apt-get install -y --no-install-recommends \\
|
|
1255
1358
|
${baselineAndToggleArgs.join(' ')} \\
|
|
1256
1359
|
&& if [ "$TARGETARCH" = "arm64" ]; then \\
|
|
@@ -1264,14 +1367,16 @@ ${LAYER_2_5_CURL_IMPERSONATE}
|
|
|
1264
1367
|
|
|
1265
1368
|
${LAYER_3_AGENT_BROWSER_ARM64_CONFIG}
|
|
1266
1369
|
|
|
1267
|
-
${
|
|
1370
|
+
${renderAgentBrowserInstallLayer(buildKit)}
|
|
1268
1371
|
|
|
1269
1372
|
${LAYER_4_5_AGENT_BROWSER_HEADED_WRAPPER}
|
|
1270
1373
|
|
|
1271
|
-
${
|
|
1374
|
+
${renderChromeForTestingLayer(buildKit)}
|
|
1272
1375
|
|
|
1273
1376
|
${cloudflaredBlock}${claudeCodeBlock}${codexCliBlock}${renderEntrypointShimLayer()}
|
|
1274
1377
|
|
|
1378
|
+
${LAYER_TRANSFORMERS_INSTALL}
|
|
1379
|
+
|
|
1275
1380
|
`
|
|
1276
1381
|
}
|
|
1277
1382
|
|
|
@@ -1291,13 +1396,11 @@ RUN ARCH_BIN="$(if [ "$TARGETARCH" = "arm64" ]; then echo arm64; else echo amd64
|
|
|
1291
1396
|
`
|
|
1292
1397
|
}
|
|
1293
1398
|
|
|
1294
|
-
function renderToggleAptInstallLayer(toggleAptArgs: string[]): string {
|
|
1399
|
+
function renderToggleAptInstallLayer(toggleAptArgs: string[], buildKit: boolean): string {
|
|
1295
1400
|
return `# Layer 1 (toggle apt install): packages requested via typeclaw.json
|
|
1296
1401
|
# #docker.file toggles. Baseline + Chrome runtime libs are already in the
|
|
1297
1402
|
# base image; this layer only adds gh/tmux/python/ffmpeg/cjkFonts if enabled.
|
|
1298
|
-
RUN
|
|
1299
|
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
1300
|
-
apt-get update \\
|
|
1403
|
+
RUN ${aptCacheMount(buildKit)}apt-get update \\
|
|
1301
1404
|
&& apt-get install -y --no-install-recommends \\
|
|
1302
1405
|
${toggleAptArgs.join(' ')}`
|
|
1303
1406
|
}
|
|
@@ -1327,9 +1430,7 @@ ${LAYER_0_APT_KEEP_CACHE}
|
|
|
1327
1430
|
# packages (gh/python/tmux/ffmpeg) are intentionally NOT installed here —
|
|
1328
1431
|
# they layer onto the base in the per-agent Dockerfile so users can opt in/
|
|
1329
1432
|
# out via typeclaw.json without forcing a base-image rebuild.
|
|
1330
|
-
RUN
|
|
1331
|
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
1332
|
-
apt-get update \\
|
|
1433
|
+
RUN ${aptCacheMount(true)}apt-get update \\
|
|
1333
1434
|
&& apt-get install -y --no-install-recommends \\
|
|
1334
1435
|
${BASELINE_APT_PACKAGES.join(' ')} \\
|
|
1335
1436
|
&& if [ "$TARGETARCH" = "arm64" ]; then \\
|
|
@@ -1343,13 +1444,15 @@ ${LAYER_2_5_CURL_IMPERSONATE}
|
|
|
1343
1444
|
|
|
1344
1445
|
${LAYER_3_AGENT_BROWSER_ARM64_CONFIG}
|
|
1345
1446
|
|
|
1346
|
-
${
|
|
1447
|
+
${renderAgentBrowserInstallLayer(true)}
|
|
1347
1448
|
|
|
1348
1449
|
${LAYER_4_5_AGENT_BROWSER_HEADED_WRAPPER}
|
|
1349
1450
|
|
|
1350
|
-
${
|
|
1451
|
+
${renderChromeForTestingLayer(true)}
|
|
1351
1452
|
|
|
1352
1453
|
${renderEntrypointShimLayer()}
|
|
1454
|
+
|
|
1455
|
+
${LAYER_TRANSFORMERS_INSTALL}
|
|
1353
1456
|
`
|
|
1354
1457
|
}
|
|
1355
1458
|
|
|
@@ -1360,6 +1463,19 @@ ${renderEntrypointShimLayer()}
|
|
|
1360
1463
|
|
|
1361
1464
|
const BUILDKIT_HEADER = `# syntax=docker/dockerfile:1.7`
|
|
1362
1465
|
|
|
1466
|
+
// Cache-mount prefixes spliced between `RUN ` and the command. BuildKit honors
|
|
1467
|
+
// `--mount=type=cache` (apt .debs / bun packages reused across builds); the
|
|
1468
|
+
// legacy builder (hosts without buildx) has no equivalent, so legacy mode emits
|
|
1469
|
+
// an empty prefix — same image, just no cross-build cache. Generating the two
|
|
1470
|
+
// modes structurally is why typeclaw needs no Dockerfile text post-processing.
|
|
1471
|
+
const APT_CACHE_MOUNT =
|
|
1472
|
+
'--mount=type=cache,target=/var/cache/apt,sharing=locked \\\n' +
|
|
1473
|
+
' --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\\n '
|
|
1474
|
+
const BUN_CACHE_MOUNT = '--mount=type=cache,target=/root/.bun/install/cache,sharing=locked \\\n '
|
|
1475
|
+
|
|
1476
|
+
const aptCacheMount = (buildKit: boolean): string => (buildKit ? APT_CACHE_MOUNT : '')
|
|
1477
|
+
const bunCacheMount = (buildKit: boolean): string => (buildKit ? BUN_CACHE_MOUNT : '')
|
|
1478
|
+
|
|
1363
1479
|
const FROM_AND_WORKDIR = `FROM oven/bun:1-slim
|
|
1364
1480
|
|
|
1365
1481
|
WORKDIR /agent
|
|
@@ -1403,10 +1519,10 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then \\
|
|
|
1403
1519
|
&& printf '%s\\n' '{"executablePath":"/usr/bin/chromium"}' > /root/.agent-browser/config.json; \\
|
|
1404
1520
|
fi`
|
|
1405
1521
|
|
|
1406
|
-
const
|
|
1522
|
+
const renderAgentBrowserInstallLayer = (buildKit: boolean): string =>
|
|
1523
|
+
`# Layer 4 (volatile): install agent-browser globally so it survives the
|
|
1407
1524
|
# runtime bind-mount over /agent/node_modules.
|
|
1408
|
-
RUN
|
|
1409
|
-
bun install -g agent-browser`
|
|
1525
|
+
RUN ${bunCacheMount(buildKit)}bun install -g agent-browser@^0.27.0`
|
|
1410
1526
|
|
|
1411
1527
|
// Layer 4.5: shim the agent-browser binary with a wrapper that calls
|
|
1412
1528
|
// \`agent-browser close\` before \`open\`/\`goto\`/\`navigate\` when headed
|
|
@@ -1522,10 +1638,9 @@ TYPECLAW_AGENT_BROWSER_WRAPPER_EOF`
|
|
|
1522
1638
|
// already installed in Layer 2; --with-deps is a defense-in-depth backstop
|
|
1523
1639
|
// so a future agent-browser bump that adds new deps installs them
|
|
1524
1640
|
// automatically (near-no-op when Layer 2 already covers them).
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
if [ "$TARGETARCH" != "arm64" ]; then \\
|
|
1641
|
+
const renderChromeForTestingLayer = (buildKit: boolean): string =>
|
|
1642
|
+
`# Layer 5 (heavy, amd64 only): Chrome for Testing download.
|
|
1643
|
+
RUN ${aptCacheMount(buildKit)}if [ "$TARGETARCH" != "arm64" ]; then \\
|
|
1529
1644
|
agent-browser install --with-deps; \\
|
|
1530
1645
|
fi`
|
|
1531
1646
|
|
|
@@ -1564,16 +1679,14 @@ function singlePackageArgs(name: string, toggle: DockerfileFeatureToggle): strin
|
|
|
1564
1679
|
// (the most frequent change) does not re-fetch the GPG key over the network.
|
|
1565
1680
|
// When `gh` is disabled, omit the layer entirely — both to skip the network
|
|
1566
1681
|
// roundtrip on cold builds and to keep the package source registry clean.
|
|
1567
|
-
function renderGhKeyringLayer(toggle: DockerfileFeatureToggle): string {
|
|
1682
|
+
function renderGhKeyringLayer(toggle: DockerfileFeatureToggle, buildKit: boolean): string {
|
|
1568
1683
|
if (toggle === false) return ''
|
|
1569
1684
|
return `# Layer 1 (rarely changes): register the GitHub CLI apt repository and trust
|
|
1570
1685
|
# its keyring. Split from the package install below so editing the package
|
|
1571
1686
|
# list (the most frequent change to this Dockerfile) does NOT re-fetch the
|
|
1572
1687
|
# GPG key over the network. The cache mount on /var/cache/apt covers the
|
|
1573
1688
|
# tiny gnupg/curl install we need to bootstrap the key fetch.
|
|
1574
|
-
RUN
|
|
1575
|
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
1576
|
-
apt-get update \\
|
|
1689
|
+
RUN ${aptCacheMount(buildKit)}apt-get update \\
|
|
1577
1690
|
&& apt-get install -y --no-install-recommends curl ca-certificates gnupg \\
|
|
1578
1691
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \\
|
|
1579
1692
|
| gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \\
|
package/src/init/gitignore.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const TRULY_IGNORED_PATTERNS = [
|
|
|
9
9
|
'.env.local',
|
|
10
10
|
'secrets.json',
|
|
11
11
|
'auth.json',
|
|
12
|
-
'.typeclaw/
|
|
12
|
+
'.typeclaw/',
|
|
13
13
|
'node_modules/',
|
|
14
14
|
'packages/*/node_modules/',
|
|
15
15
|
'workspace/',
|
|
@@ -40,12 +40,12 @@ export function buildGitignore(config: GitignoreConfig = { append: [] }): string
|
|
|
40
40
|
# as a safety net so an agent folder cloned from a pre-rename machine never
|
|
41
41
|
# stages credentials by accident.
|
|
42
42
|
#
|
|
43
|
-
# .typeclaw/
|
|
44
|
-
# entrypoint shim's
|
|
45
|
-
# src/init/dockerfile.ts
|
|
46
|
-
# $HOME
|
|
47
|
-
#
|
|
48
|
-
# commit.
|
|
43
|
+
# .typeclaw/ is the agent's local-scratch dir. It holds the persistent-$HOME
|
|
44
|
+
# overlay (.typeclaw/home/, populated by the entrypoint shim's
|
|
45
|
+
# \`link_persistent_home_files\` — see src/init/dockerfile.ts, mirrors container
|
|
46
|
+
# $HOME files like ~/.codex/auth.json so tool credentials survive restarts) and
|
|
47
|
+
# the in-progress init checkpoint (.typeclaw/init-progress.json, non-secret
|
|
48
|
+
# wizard selections for resume). Local-only; never commit.
|
|
49
49
|
${TRULY_IGNORED_PATTERNS.join('\n')}
|
|
50
50
|
|
|
51
51
|
# System-managed: gitignored by default so the agent never stages them by hand,
|