@tealbrick/kit 0.2.7 → 0.3.0-rc.10

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 (46) hide show
  1. package/BOOTSTRAP.md +17 -18
  2. package/BUZZ.md +146 -0
  3. package/NATIVE.md +83 -0
  4. package/README.md +265 -4
  5. package/RUNTIME.md +1 -1
  6. package/assets/buzz/buzz-backend-provider.py +99 -0
  7. package/assets/buzz/buzz-plist.py +22 -0
  8. package/assets/buzz/buzz-run.sh +39 -0
  9. package/assets/buzz/tb-provider-apply.py +97 -0
  10. package/dist/buzz.d.ts +63 -0
  11. package/dist/buzz.js +262 -0
  12. package/dist/cli.js +22 -4
  13. package/dist/index.d.ts +23 -14
  14. package/dist/index.js +4 -2
  15. package/dist/native-acp.d.ts +40 -0
  16. package/dist/native-acp.js +224 -0
  17. package/dist/native-app-capabilities.d.ts +33 -0
  18. package/dist/native-app-capabilities.js +37 -0
  19. package/dist/native-buzz.d.ts +161 -0
  20. package/dist/native-buzz.js +649 -0
  21. package/dist/native-child-guard.d.ts +1 -0
  22. package/dist/native-child-guard.js +22 -0
  23. package/dist/native-claude.d.ts +99 -0
  24. package/dist/native-claude.js +584 -0
  25. package/dist/native-enroll.d.ts +22 -0
  26. package/dist/native-enroll.js +85 -0
  27. package/dist/native-runtime-sync.d.ts +49 -0
  28. package/dist/native-runtime-sync.js +101 -0
  29. package/dist/native-selection.d.ts +27 -0
  30. package/dist/native-selection.js +87 -0
  31. package/dist/native-serve-config.d.ts +286 -0
  32. package/dist/native-serve-config.js +296 -0
  33. package/dist/native-serve.d.ts +76 -0
  34. package/dist/native-serve.js +145 -0
  35. package/dist/native-setup.d.ts +63 -0
  36. package/dist/native-setup.js +122 -0
  37. package/dist/native-standing-grants.d.ts +105 -0
  38. package/dist/native-standing-grants.js +226 -0
  39. package/dist/native.d.ts +42 -0
  40. package/dist/native.js +193 -0
  41. package/dist/onboarding.d.ts +9 -0
  42. package/dist/onboarding.js +9 -2
  43. package/dist/runtime-onboarding.js +3 -3
  44. package/dist/secrets.d.ts +2 -1
  45. package/dist/secrets.js +4 -2
  46. package/package.json +19 -10
@@ -0,0 +1,296 @@
1
+ import { z } from 'zod';
2
+ import { lstat, readFile, realpath, writeFile, rename, mkdir } from 'node:fs/promises';
3
+ import { isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ /** Operator configuration for `tealbrick native serve`. Contains no credentials. */
6
+ const absolute = z.string().min(1).refine(v => isAbsolute(v) && !v.includes('\0'), 'absolute path required');
7
+ export const hostPattern = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*(?::\d{1,5})?$/i;
8
+ const mcpTool = z.string().regex(/^mcp__[a-zA-Z0-9_-]+__[a-zA-Z0-9_-]+$/);
9
+ const tool = z.union([z.string().regex(/^[A-Z][A-Za-z]{1,40}$/), mcpTool]);
10
+ const serverName = z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/).refine(n => n !== 'tealbrick', 'the tealbrick MCP server comes from .tealbrick/native-sdk.json');
11
+ const stdio = z.object({ type: z.literal('stdio').optional(), command: z.string().min(1), args: z.array(z.string()).optional(), env: z.record(z.string(), z.string()).optional() }).strict();
12
+ export const tealbrickCallGrant = z.object({ registrationId: z.string().min(1), operation: z.string().min(1), inputEquals: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])).default({}) }).strict();
13
+ const effort = z.enum(['low', 'medium', 'high', 'xhigh', 'max']);
14
+ /** Tools an agent may add to its default (Restricted) session access: research and expert delegation, never shell or writes. */
15
+ export const defaultAccessTools = ['WebSearch', 'WebFetch', 'Agent', 'TodoWrite'];
16
+ /** Tools an expert subagent may hold; workspace reads stay contained and network content stays untrusted. */
17
+ const expertTool = z.enum(['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch']);
18
+ const expertName = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/);
19
+ /** Sandboxed owner workspace: all file work runs through the sandboxed Bash inside `dir`; shell network limited to allowedDomains. */
20
+ const domain = z.string().regex(/^(?:\*\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/, 'lowercase host name or *.host');
21
+ const envName = z.string().regex(/^[A-Z][A-Z0-9_]{0,63}$/);
22
+ const workspace = z.object({ dir: absolute, network: z.object({ allowedDomains: z.array(domain).max(64) }).strict().optional(),
23
+ /** envPassthrough names that are NOT secret and may stay visible to sandboxed shell commands (all others are removed there). */
24
+ envAllow: z.array(envName).max(32).optional() }).strict();
25
+ /** Versioned expert contract (tealbrick.expert-contract/v1). The contract is a ceiling; Portal grants still apply. */
26
+ const opName = z.string().regex(/^[a-z][a-z0-9_.]{1,120}$/);
27
+ export const expertContractSchema = z.object({
28
+ schema: z.literal('tealbrick.expert-contract/v1'),
29
+ id: z.string().regex(/^[a-z][a-z0-9-]{1,62}$/),
30
+ version: z.string().regex(/^\d+\.\d+\.\d+$/),
31
+ purpose: z.string().min(10).max(300),
32
+ instructions: z.string().min(40).max(8000),
33
+ input: z.object({ type: z.literal('object') }).passthrough(),
34
+ output: z.object({ type: z.literal('object') }).passthrough(),
35
+ capabilities: z.object({
36
+ builtinTools: z.array(expertTool).max(8),
37
+ apps: z.array(z.object({ appId: z.string().regex(/^[a-z][a-z0-9-]{1,40}$/), operations: z.array(z.string().regex(/^[a-z][a-z0-9_]{1,80}$/)).min(1) }).strict()).max(12),
38
+ marketplace: z.array(z.object({ plugin: z.string().regex(/^[a-z0-9][a-z0-9-]{1,80}$/), actions: z.array(z.string().regex(/^[a-z0-9][a-z0-9._-]{1,120}$/)).min(1) }).strict()).max(12),
39
+ }).strict(),
40
+ approval: z.object({ outwardWrites: z.enum(['always', 'never-allowed']), operations: z.array(opName) }).strict(),
41
+ model: z.enum(['inherit', 'opus', 'sonnet', 'haiku']),
42
+ effort: z.enum(['low', 'medium', 'high']).optional(),
43
+ budget: z.object({ maxTurns: z.number().int().min(1).max(50) }).strict(),
44
+ runtime: z.enum(['subagent', 'desktop-operator']),
45
+ changelog: z.array(z.string().max(300)).optional(),
46
+ }).strict();
47
+ /** Simple glob: exact text, or '*' for any run of characters. */
48
+ const globText = z.string().min(1).max(200);
49
+ const globList = z.union([globText, z.array(globText).min(1).max(32)]);
50
+ /**
51
+ * Owner-defined standing grant: a bounded class of calls that would otherwise pause for payload approval
52
+ * and that the kit hook approves without a prompt, within caps, until it expires. Never a wider scope than
53
+ * decideTool already allows; it only replaces the per-payload prompt.
54
+ */
55
+ export const standingGrantSchema = z.object({
56
+ id: z.string().regex(/^[a-z0-9][a-z0-9-]{1,47}$/),
57
+ description: z.string().min(1).max(300),
58
+ operation: opName.default('marketplace_execute'),
59
+ /** registrationId is case-sensitive; plugin/action are case-insensitive. All support '*'. At least one is required. */
60
+ match: z.object({ registrationId: globText.optional(), plugin: globList.optional(), action: globList.optional() }).strict()
61
+ .refine(m => m.registrationId !== undefined || m.plugin !== undefined || m.action !== undefined, 'match needs registrationId, plugin or action (use "*" to mean any)'),
62
+ /** Contract ids whose expert subagents may also use this grant. Omitted: the owner session only. */
63
+ experts: z.array(z.string().regex(/^[a-z][a-z0-9-]{1,62}$/)).max(32).optional(),
64
+ caps: z.object({ perDay: z.number().int().min(1).max(200), perHour: z.number().int().min(1).max(200).optional(), minIntervalSeconds: z.number().int().min(1).max(86400).optional() }).strict(),
65
+ /** ISO 8601 date-time with offset. Expired grants never match. */
66
+ expires: z.iso.datetime({ offset: true }),
67
+ /** Case-insensitive literal substrings of JSON.stringify(call input); any hit means the grant does not apply. */
68
+ deny: z.object({ inputPattern: z.array(z.string().min(1).max(200)).max(32).optional() }).strict().optional(),
69
+ }).strict();
70
+ const expert = z.object({ promptFile: absolute, description: z.string().min(1).max(300).optional(), tools: z.array(expertTool).max(8).optional(), maxTurns: z.number().int().min(1).max(50).optional(), effort: effort.optional() }).strict();
71
+ const claude = z.object({
72
+ /** Package directory or entry file of @anthropic-ai/claude-agent-sdk. Default: resolved from the kit root. */
73
+ sdkModule: absolute.optional(),
74
+ /** Working directory. Claude session transcripts are keyed by it; keep it stable. */
75
+ cwd: absolute,
76
+ instructionsFile: absolute.optional(),
77
+ /** Import model/limits/MCP/read tools/app-call ceilings from an existing tealbrick-native recipe file. */
78
+ recipe: z.object({ path: absolute, agent: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/) }).strict().optional(),
79
+ model: z.string().min(1).max(100).optional(),
80
+ effort: effort.optional(),
81
+ maxTurns: z.number().int().min(1).max(200).optional(),
82
+ maxBudgetUsd: z.number().positive().max(100).optional(),
83
+ timeoutSeconds: z.number().int().min(10).max(7200).optional(),
84
+ /** Extra tools in this agent's default (Restricted) session access. */
85
+ defaultTools: z.array(z.enum(defaultAccessTools)).max(4).optional(),
86
+ /** Expert subagents the Agent tool may delegate to, foreground only, inheriting the session model. */
87
+ experts: z.record(expertName, expert).optional(),
88
+ /** Versioned expert contracts (JSON files) run as SDK subagents. Effective scope = contract ceiling ∩ owner grants. */
89
+ contracts: z.array(absolute).max(32).optional(),
90
+ /** Operations that always pause for the owner's approval of the exact payload, whoever calls them. */
91
+ approvalOperations: z.array(opName).max(64).optional(),
92
+ /** Owner pre-approvals for bounded classes of approval-gated calls (see README "Standing grants"). */
93
+ standingGrants: z.array(standingGrantSchema).max(64).optional()
94
+ .refine(g => !g || new Set(g.map(x => x.id)).size === g.length, 'standing grant ids must be unique'),
95
+ /** Tool allowlist for the opt-in "Native defaults" session access mode. */
96
+ nativeTools: z.array(tool).max(64).optional(),
97
+ /** Operator-vetted read-only MCP tools, also available in Restricted mode. */
98
+ readTools: z.array(mcpTool).max(32).optional(),
99
+ mcpServers: z.record(serverName, stdio).optional(),
100
+ tealbrickCalls: z.array(tealbrickCallGrant).max(64).optional(),
101
+ /** Refuse turns unless the SDK reports first-party subscription sign-in (no API-key fallback). */
102
+ requireSubscription: z.boolean().optional(),
103
+ /** Opt-in sandboxed workspace for the owner session (inside the kit root, never containing .tealbrick). */
104
+ workspace: workspace.optional(),
105
+ /** Extra environment variable names passed to the SDK process. TEALBRICK_* is never passed. */
106
+ envPassthrough: z.array(envName.refine(n => !n.startsWith('TEALBRICK_'), 'Teal Brick credentials are never passed to the harness')).max(32).optional(),
107
+ }).strict();
108
+ const codex = z.object({ codexBin: absolute, cwd: absolute }).strict();
109
+ export const serveConfigSchema = z.object({
110
+ version: z.literal(1),
111
+ port: z.number().int().min(1024).max(65535),
112
+ /** Public HTTPS origin registered on Portal (for example a tailscale serve URL). */
113
+ publicUrl: z.string().url().optional(),
114
+ /** Extra exact Host header values. The public URL host and loopback are added automatically. */
115
+ allowedHosts: z.array(z.string().regex(hostPattern)).max(8).optional(),
116
+ maxActiveTurns: z.number().int().min(1).max(16).optional(),
117
+ /** Written by `native enroll`: Portal identity used to verify every request. */
118
+ agent: z.object({ id: z.string().min(1), name: z.string().min(1).max(80), ownerId: z.string().min(1) }).strict().optional(),
119
+ claude: claude.optional(),
120
+ codex: codex.optional(),
121
+ }).strict();
122
+ export const servePaths = (root) => ({ config: join(root, '.tealbrick', 'native-serve.json'), state: join(root, '.tealbrick', 'native-serve'), sdk: join(root, '.tealbrick', 'native-sdk.json'),
123
+ /** Kit-owned attachment inbox: written only by the kit process, readable (never writable) by the sandboxed workspace session. */
124
+ inbox: join(root, 'inbox') });
125
+ async function regularFile(path, { ownerOnly }) {
126
+ const stat = await lstat(path);
127
+ if (!stat.isFile() || stat.isSymbolicLink())
128
+ throw Error('kit_native_serve_unsafe_file');
129
+ if (process.platform !== 'win32' && (stat.mode & (ownerOnly ? 0o077 : 0o022)))
130
+ throw Error('kit_native_serve_file_permissions');
131
+ }
132
+ /** Stable, value-free error codes for operator files; never echo file contents. */
133
+ function parsed(schema, text, code) {
134
+ let value;
135
+ try {
136
+ value = JSON.parse(text);
137
+ }
138
+ catch {
139
+ throw Error(code);
140
+ }
141
+ const result = schema.safeParse(value);
142
+ if (!result.success)
143
+ throw Error(code + ':' + result.error.issues.map(i => i.path.join('.') || 'root').slice(0, 5).join(','));
144
+ return result.data;
145
+ }
146
+ export function normalizeOrigin(value) {
147
+ const url = new URL(value);
148
+ const loopback = ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname);
149
+ if (!(url.protocol === 'https:' || url.protocol === 'http:' && loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/')
150
+ throw Error('kit_native_public_url_https_origin_required');
151
+ return url.origin;
152
+ }
153
+ /** Host header values a TLS-terminating proxy for `publicUrl` may present. */
154
+ export function publicHosts(config) {
155
+ const hosts = [...(config.allowedHosts ?? [])];
156
+ if (config.publicUrl) {
157
+ const url = new URL(config.publicUrl);
158
+ hosts.push(url.host);
159
+ if (!url.port)
160
+ hosts.push(url.hostname + ':' + (url.protocol === 'https:' ? 443 : 80));
161
+ }
162
+ return [...new Set(hosts.map(h => h.toLowerCase()))];
163
+ }
164
+ export async function readServeConfig(root) {
165
+ const path = servePaths(root).config;
166
+ try {
167
+ await regularFile(path, { ownerOnly: true });
168
+ }
169
+ catch (e) {
170
+ if (e.code === 'ENOENT')
171
+ return undefined;
172
+ throw e;
173
+ }
174
+ const config = parsed(serveConfigSchema, await readFile(path, 'utf8'), 'kit_native_serve_config_invalid');
175
+ if (config.publicUrl && normalizeOrigin(config.publicUrl) !== config.publicUrl)
176
+ throw Error('kit_native_public_url_https_origin_required');
177
+ return config;
178
+ }
179
+ export async function writeServeConfig(root, config) {
180
+ const value = serveConfigSchema.parse(config), { config: path } = servePaths(root);
181
+ await mkdir(join(root, '.tealbrick'), { recursive: true, mode: 0o700 });
182
+ try {
183
+ if ((await lstat(path)).isSymbolicLink())
184
+ throw Error('kit_symlink_path');
185
+ }
186
+ catch (e) {
187
+ if (e.code !== 'ENOENT')
188
+ throw e;
189
+ }
190
+ const tmp = path + '.' + randomUUID();
191
+ await writeFile(tmp, JSON.stringify(value, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
192
+ await rename(tmp, path);
193
+ }
194
+ /** The kit-generated Claude attachment: tealbrick MCP server plus the child guard hook. */
195
+ const generatedClaude = z.object({
196
+ settings: z.object({ hooks: z.object({ PreToolUse: z.array(z.object({ matcher: z.literal('mcp__tealbrick__.*'), hooks: z.array(z.object({ type: z.literal('command'), command: z.string().min(1), args: z.array(z.string()).min(1) }).strict()).min(1) }).strict()).min(1) }).strict() }).strict(),
197
+ mcpServers: z.object({ tealbrick: z.object({ command: z.string().min(1), args: z.array(z.string()).min(1) }).strict() }).strict(),
198
+ }).strict();
199
+ export async function readClaudeAttachment(root) {
200
+ const path = servePaths(root).sdk;
201
+ await regularFile(path, { ownerOnly: true });
202
+ const value = parsed(generatedClaude, await readFile(path, 'utf8'), 'kit_native_sdk_config_invalid');
203
+ const args = value.mcpServers.tealbrick.args, at = args.indexOf('--root');
204
+ if (args.slice(-4, -2).join(' ') !== 'native mcp' || at < 0 || resolve(args[at + 1] ?? '') !== await realpath(root))
205
+ throw Error('kit_native_sdk_config_root_mismatch');
206
+ return value;
207
+ }
208
+ const recipeSchema = z.object({
209
+ model: z.string().min(1).optional(), effort: effort.optional(), maxTurns: z.number().int().min(1).max(200).optional(),
210
+ maxBudgetUsd: z.number().positive().max(100).optional(), timeoutSeconds: z.number().int().min(10).max(7200).optional(),
211
+ agents: z.record(z.string(), z.object({ mcpServers: z.record(serverName, stdio).optional(), readTools: z.array(mcpTool).max(32).optional(), tealbrickCalls: z.array(tealbrickCallGrant).max(64).optional(), tealbrickSdkConfig: absolute.optional() }).passthrough()),
212
+ }).passthrough();
213
+ /** Resolve the effective Claude profile. Explicit serve fields override the imported recipe. */
214
+ export async function resolveClaudeProfile(root, config) {
215
+ const c = config.claude;
216
+ if (!c)
217
+ throw Error('kit_native_serve_claude_profile_required');
218
+ let recipe, agent;
219
+ if (c.recipe) {
220
+ await regularFile(c.recipe.path, { ownerOnly: false });
221
+ recipe = parsed(recipeSchema, await readFile(c.recipe.path, 'utf8'), 'kit_native_serve_recipe_invalid');
222
+ agent = recipe.agents[c.recipe.agent];
223
+ if (!agent)
224
+ throw Error('kit_native_serve_recipe_agent_missing');
225
+ // The recipe must describe THIS kit root's Portal connection, never another agent's.
226
+ if (agent.tealbrickSdkConfig && await realpath(agent.tealbrickSdkConfig) !== await realpath(servePaths(root).sdk))
227
+ throw Error('kit_native_serve_recipe_connection_mismatch');
228
+ }
229
+ let cwd = resolve(c.cwd);
230
+ const protectedRoot = join(await realpath(root), '.tealbrick');
231
+ if (protectedRoot.startsWith(cwd + '/') || cwd === protectedRoot || cwd.startsWith(protectedRoot + '/'))
232
+ throw Error('kit_native_serve_cwd_contains_credentials');
233
+ if (!(await lstat(cwd).catch(() => undefined))?.isDirectory())
234
+ throw Error('kit_native_serve_cwd_missing');
235
+ const instructions = c.instructionsFile ? (await regularFile(c.instructionsFile, { ownerOnly: false }), await readFile(c.instructionsFile, 'utf8')) : undefined;
236
+ if (instructions && instructions.length > 200000)
237
+ throw Error('kit_native_serve_instructions_too_large');
238
+ let workspace;
239
+ if (c.workspace) {
240
+ const realRoot = await realpath(root), inside = (base, p) => { const rel = relative(base, p); return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel); };
241
+ const dir = resolve(c.workspace.dir), tb = (base) => join(base, '.tealbrick');
242
+ const lexical = (base) => inside(base, dir) && dir !== tb(base) && !inside(tb(base), dir);
243
+ if (!lexical(realRoot) && !lexical(resolve(root)))
244
+ throw Error('kit_native_serve_workspace_outside_root');
245
+ // The attachment inbox must stay outside anything the sandbox can write.
246
+ const overlapsInbox = (base, p) => { const inbox = servePaths(base).inbox; return p === inbox || inside(inbox, p); };
247
+ if (overlapsInbox(realRoot, dir) || overlapsInbox(resolve(root), dir))
248
+ throw Error('kit_native_serve_workspace_overlaps_inbox');
249
+ await mkdir(dir, { recursive: true, mode: 0o700 });
250
+ const real = await realpath(dir);
251
+ if (!inside(realRoot, real) || real === tb(realRoot) || inside(tb(realRoot), real))
252
+ throw Error('kit_native_serve_workspace_outside_root');
253
+ if (overlapsInbox(realRoot, real))
254
+ throw Error('kit_native_serve_workspace_overlaps_inbox');
255
+ if (await lstat(join(real, '.tealbrick')).then(() => true, () => false))
256
+ throw Error('kit_native_serve_workspace_contains_credentials');
257
+ workspace = { dir: real, root: realRoot, allowedDomains: [...new Set(c.workspace.network?.allowedDomains ?? [])], envAllow: [...new Set(c.workspace.envAllow ?? [])] };
258
+ cwd = real;
259
+ }
260
+ const readTools = c.readTools ?? agent?.readTools ?? [];
261
+ const contracts = {};
262
+ for (const path of c.contracts ?? []) {
263
+ await regularFile(path, { ownerOnly: false });
264
+ const text = await readFile(path, 'utf8');
265
+ if (text.length > 65536)
266
+ throw Error('kit_native_serve_contract_too_large');
267
+ const contract = parsed(expertContractSchema, text, 'kit_native_serve_contract_invalid');
268
+ if (contract.runtime !== 'subagent')
269
+ throw Error('kit_native_serve_contract_runtime_unsupported:' + contract.id);
270
+ if (Object.hasOwn(contracts, contract.id))
271
+ throw Error('kit_native_serve_contract_duplicate:' + contract.id);
272
+ contracts[contract.id] = contract;
273
+ }
274
+ const experts = {};
275
+ for (const [name, e] of Object.entries(c.experts ?? {})) {
276
+ await regularFile(e.promptFile, { ownerOnly: false });
277
+ const prompt = await readFile(e.promptFile, 'utf8');
278
+ if (!prompt.trim() || prompt.length > 50000)
279
+ throw Error('kit_native_serve_expert_prompt_invalid:' + name);
280
+ experts[name] = { description: e.description ?? name.replaceAll('-', ' ') + ' for a bounded assignment', prompt, tools: [...new Set(e.tools ?? [])], maxTurns: e.maxTurns ?? 8, ...(e.effort ? { effort: e.effort } : {}) };
281
+ }
282
+ for (const id of Object.keys(contracts))
283
+ if (Object.hasOwn(experts, id))
284
+ throw Error('kit_native_serve_contract_duplicate:' + id);
285
+ const standingGrants = c.standingGrants ?? [];
286
+ for (const g of standingGrants)
287
+ for (const id of g.experts ?? [])
288
+ if (!Object.hasOwn(contracts, id))
289
+ throw Error('kit_native_serve_standing_grant_unknown_expert:' + g.id);
290
+ return { cwd, sdkModule: c.sdkModule, instructions, model: c.model ?? recipe?.model, effort: c.effort ?? recipe?.effort,
291
+ maxTurns: c.maxTurns ?? recipe?.maxTurns ?? 24, maxBudgetUsd: c.maxBudgetUsd ?? recipe?.maxBudgetUsd, timeoutSeconds: c.timeoutSeconds ?? recipe?.timeoutSeconds ?? 900,
292
+ defaultTools: [...new Set(c.defaultTools ?? [])], experts, contracts, approvalOperations: [...new Set(c.approvalOperations ?? ['marketplace_execute'])], standingGrants,
293
+ nativeTools: c.nativeTools ?? ['Read', 'Glob', 'Grep', 'TodoWrite'], readTools: readTools.filter(t => !t.startsWith('mcp__tealbrick__')),
294
+ mcpServers: c.mcpServers ?? agent?.mcpServers ?? {}, tealbrickCalls: c.tealbrickCalls ?? agent?.tealbrickCalls ?? [],
295
+ requireSubscription: c.requireSubscription ?? false, envPassthrough: c.envPassthrough ?? [], ...(workspace ? { workspace } : {}) };
296
+ }
@@ -0,0 +1,76 @@
1
+ import { type AuthResult } from '@tealbrick/portal';
2
+ import { type ClaudeSdk } from './native-claude.js';
3
+ import { type RuntimeSyncOptions } from './native-runtime-sync.js';
4
+ import { type AppCapabilitiesOptions } from './native-app-capabilities.js';
5
+ import { type GrantStoreOptions } from './native-standing-grants.js';
6
+ /** LangChain has no stable session/stream/cancel contract to drive from here yet. */
7
+ export declare function langchainHarness(): never;
8
+ export interface ServeDependencies {
9
+ authenticate?: (request: Request) => Promise<AuthResult>;
10
+ claudeSdk?: ClaudeSdk;
11
+ log?: (event: Record<string, unknown>) => void;
12
+ env?: NodeJS.ProcessEnv;
13
+ /** Background Portal config/ack sync for this runtime; false disables it (tests only). */
14
+ runtimeSync?: RuntimeSyncOptions | false;
15
+ /** Test seams for the owner-only /tealbrick/v1/capabilities snapshot. */
16
+ appCapabilities?: AppCapabilitiesOptions;
17
+ approvalTimeoutMs?: number;
18
+ /** Test seams for the standing-grant usage store (clock, lock timing). */
19
+ standingGrants?: GrantStoreOptions;
20
+ }
21
+ /** Build (but do not bind) the owner-authenticated chat endpoint for this kit root. */
22
+ export declare function createNativeServe(root: string, deps?: ServeDependencies): Promise<{
23
+ port: number;
24
+ harness: "claude" | "codex" | "langchain";
25
+ runtime: {
26
+ snapshot: () => import("./native-runtime-sync.js").RuntimeSyncSnapshot;
27
+ capabilities: () => {
28
+ revision: string;
29
+ expiresAt: number;
30
+ apps: {
31
+ registrationId: string;
32
+ appId: "knowledge";
33
+ actions: import("@tealbrick/portal/runtime").CrudAction[];
34
+ available: boolean;
35
+ operations: import("@tealbrick/portal/runtime").KnowledgeOperation[];
36
+ }[];
37
+ } | undefined;
38
+ syncNow: () => Promise<void>;
39
+ stop(): Promise<void>;
40
+ } | undefined;
41
+ close: () => Promise<void>;
42
+ server: import("http").Server;
43
+ sessions: Map<string, import("@tealbrick/portal/native-bridge").BridgeSession<any>>;
44
+ }>;
45
+ export declare function serveNative(root: string): Promise<void>;
46
+ export declare function launchdLabel(root: string): string;
47
+ /** User LaunchAgent: restarts on crash, logs to the kit root's private state directory. */
48
+ export declare function launchdPlist(root: string, options?: {
49
+ node?: string;
50
+ cli?: string;
51
+ }): string;
52
+ type Exec = (file: string, args: string[]) => Promise<unknown>;
53
+ export declare function installLaunchd(root: string, options?: {
54
+ home?: string;
55
+ exec?: Exec;
56
+ load?: boolean;
57
+ uid?: number;
58
+ platform?: string;
59
+ }): Promise<{
60
+ installed: boolean;
61
+ label: string;
62
+ plist: string;
63
+ loaded: boolean;
64
+ logs: string;
65
+ }>;
66
+ export declare function uninstallLaunchd(root: string, options?: {
67
+ home?: string;
68
+ exec?: Exec;
69
+ uid?: number;
70
+ platform?: string;
71
+ }): Promise<{
72
+ uninstalled: boolean;
73
+ label: string;
74
+ plist: string;
75
+ }>;
76
+ export {};
@@ -0,0 +1,145 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { execFile } from 'node:child_process';
3
+ import { mkdir, realpath, writeFile, rename, unlink, lstat } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { promisify } from 'node:util';
8
+ import { createPortalVerifier } from '@tealbrick/portal';
9
+ import { createNativeBridge } from '@tealbrick/portal/native-bridge';
10
+ import { CodexAppServer, codexHarness } from '@tealbrick/portal/native-codex';
11
+ import { readKit } from './index.js';
12
+ import { nativeSdkConfig } from './native-setup.js';
13
+ import { claudeHarness, loadClaudeSdk } from './native-claude.js';
14
+ import { startRuntimeSync } from './native-runtime-sync.js';
15
+ import { appCapabilitiesProvider } from './native-app-capabilities.js';
16
+ import { publicHosts, readClaudeAttachment, readServeConfig, resolveClaudeProfile, servePaths } from './native-serve-config.js';
17
+ import { standingGrantStore, standingGrantSummary } from './native-standing-grants.js';
18
+ /** LangChain has no stable session/stream/cancel contract to drive from here yet. */
19
+ export function langchainHarness() { throw Error('kit_native_serve_langchain_unsupported'); }
20
+ /** Build (but do not bind) the owner-authenticated chat endpoint for this kit root. */
21
+ export async function createNativeServe(root, deps = {}) {
22
+ root = await realpath(root);
23
+ const kit = await readKit(root);
24
+ if (!kit.native || !kit.runtime)
25
+ throw Error('kit_native_setup_required');
26
+ if (!kit.native.active)
27
+ throw Error('runtime_product_inactive');
28
+ const config = await readServeConfig(root);
29
+ if (!config)
30
+ throw Error('kit_native_serve_config_required');
31
+ if (!config.agent)
32
+ throw Error('kit_native_serve_enroll_required');
33
+ if (config.agent.id !== kit.runtime.agentId)
34
+ throw Error('kit_native_serve_identity_mismatch');
35
+ const paths = servePaths(root), log = deps.log;
36
+ await mkdir(paths.state, { recursive: true, mode: 0o700 });
37
+ let adapter;
38
+ let codex;
39
+ let runtime;
40
+ let grantSummary;
41
+ const appCapabilities = appCapabilitiesProvider(root, () => runtime, { ...deps.appCapabilities, ...(kit.native.harness === 'claude' ? { standingGrants: async () => grantSummary?.() ?? [] } : {}) });
42
+ if (kit.native.harness === 'claude') {
43
+ const profile = await resolveClaudeProfile(root, config);
44
+ // Shared with `native acp` on this kit root (same file, lock-guarded).
45
+ const grants = standingGrantStore(join(paths.state, 'standing-grants.json'), deps.standingGrants);
46
+ grantSummary = () => standingGrantSummary(profile.standingGrants, grants);
47
+ // Contract scope checks resolve registration ids from this runtime's own Portal-granted snapshot.
48
+ const resolveApp = async (registrationId) => {
49
+ const snap = await appCapabilities();
50
+ const app = snap.apps.find(a => a.registrationId === registrationId);
51
+ if (app)
52
+ return { appId: app.appId };
53
+ const grant = snap.marketplace.grants.find(g => g.registrationId === registrationId);
54
+ return grant ? { appId: 'marketplace', plugin: grant.plugin, action: grant.action } : undefined;
55
+ };
56
+ adapter = claudeHarness({ name: config.agent.name, profile, attachment: await readClaudeAttachment(root), sdk: deps.claudeSdk ?? await loadClaudeSdk(root, profile.sdkModule), protectedPaths: [join(root, '.tealbrick')], log, env: deps.env, resolveApp, traceDir: join(paths.state, 'traces'), standingGrants: grants, ...(deps.approvalTimeoutMs ? { approvalTimeoutMs: deps.approvalTimeoutMs } : {}) });
57
+ }
58
+ else if (kit.native.harness === 'codex') {
59
+ if (!config.codex)
60
+ throw Error('kit_native_serve_codex_profile_required');
61
+ codex = new CodexAppServer(config.codex.codexBin, config.codex.cwd);
62
+ await codex.start();
63
+ const mcp = nativeSdkConfig('codex', root, { codexDelegationDisabled: true }).config.mcp_servers.tealbrick;
64
+ adapter = codexHarness(codex, { name: config.agent.name, cwd: config.codex.cwd, mcp, features: { multi_agent_v2: false }, log });
65
+ }
66
+ else
67
+ adapter = langchainHarness();
68
+ const authenticate = deps.authenticate ?? createPortalVerifier({ issuer: kit.runtime.issuer, org: kit.runtime.org, agent: config.agent.name }).authenticate;
69
+ const bridge = await createNativeBridge({ name: config.agent.name, ownerId: config.agent.ownerId, root: paths.state, port: config.port, allowedHosts: publicHosts(config), authenticate, adapter, maxActiveTurns: config.maxActiveTurns ?? 2, log, appCapabilities });
70
+ // Portal marks a runtime expired unless it re-acknowledges its signed config within the 300s lease;
71
+ // an idle serve process has no per-turn MCP child doing that, so it keeps the lease itself.
72
+ runtime = deps.runtimeSync === false ? undefined : startRuntimeSync(root, { log, ...deps.runtimeSync });
73
+ return { ...bridge, port: config.port, harness: kit.native.harness, runtime, close: async () => { await runtime?.stop(); await bridge.close(); await codex?.close(); } };
74
+ }
75
+ export async function serveNative(root) {
76
+ const serve = await createNativeServe(root);
77
+ // Loopback only. Remote reachability is a TLS-terminating proxy such as tailscale serve.
78
+ await new Promise((resolve, reject) => { serve.server.once('error', reject); serve.server.listen(serve.port, '127.0.0.1', () => resolve()); });
79
+ process.stderr.write(JSON.stringify({ event: 'native.serve.ready', harness: serve.harness, port: serve.port }) + '\n');
80
+ for (const signal of ['SIGTERM', 'SIGINT'])
81
+ process.once(signal, () => { void serve.close().finally(() => process.exit(0)); });
82
+ }
83
+ const xml = (v) => v.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
84
+ export function launchdLabel(root) { return 'com.tealbrick.native-serve.' + createHash('sha256').update(root).digest('hex').slice(0, 12); }
85
+ /** User LaunchAgent: restarts on crash, logs to the kit root's private state directory. */
86
+ export function launchdPlist(root, options = {}) {
87
+ const node = options.node ?? process.execPath, cli = options.cli ?? fileURLToPath(new URL('./cli.js', import.meta.url)), logs = join(servePaths(root).state, 'logs');
88
+ const args = [node, cli, 'native', 'serve', '--root', root];
89
+ return `<?xml version="1.0" encoding="UTF-8"?>
90
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
91
+ <plist version="1.0">
92
+ <dict>
93
+ <key>Label</key><string>${xml(launchdLabel(root))}</string>
94
+ <key>ProgramArguments</key><array>${args.map(a => `<string>${xml(a)}</string>`).join('')}</array>
95
+ <key>WorkingDirectory</key><string>${xml(root)}</string>
96
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(dirname(node))}:/usr/bin:/bin:/usr/sbin:/sbin</string></dict>
97
+ <key>RunAtLoad</key><true/>
98
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
99
+ <key>ThrottleInterval</key><integer>30</integer>
100
+ <key>ProcessType</key><string>Background</string>
101
+ <key>Umask</key><integer>63</integer>
102
+ <key>StandardOutPath</key><string>${xml(join(logs, 'serve.out.log'))}</string>
103
+ <key>StandardErrorPath</key><string>${xml(join(logs, 'serve.err.log'))}</string>
104
+ </dict>
105
+ </plist>
106
+ `;
107
+ }
108
+ export async function installLaunchd(root, options = {}) {
109
+ if ((options.platform ?? process.platform) !== 'darwin')
110
+ throw Error('kit_launchd_macos_only');
111
+ root = await realpath(root);
112
+ const config = await readServeConfig(root);
113
+ if (!config?.agent)
114
+ throw Error('kit_native_serve_enroll_required');
115
+ const label = launchdLabel(root), dir = join(options.home ?? homedir(), 'Library', 'LaunchAgents'), path = join(dir, label + '.plist');
116
+ await mkdir(join(servePaths(root).state, 'logs'), { recursive: true, mode: 0o700 });
117
+ await mkdir(dir, { recursive: true });
118
+ try {
119
+ if ((await lstat(path)).isSymbolicLink())
120
+ throw Error('kit_symlink_path');
121
+ }
122
+ catch (e) {
123
+ if (e.code !== 'ENOENT')
124
+ throw e;
125
+ }
126
+ await writeFile(path + '.tmp', launchdPlist(root), { mode: 0o600 });
127
+ await rename(path + '.tmp', path);
128
+ const exec = options.exec ?? promisify(execFile), domain = 'gui/' + (options.uid ?? process.getuid?.() ?? 0);
129
+ if (options.load !== false) {
130
+ await exec('launchctl', ['bootout', domain + '/' + label]).catch(() => { });
131
+ await exec('launchctl', ['bootstrap', domain, path]);
132
+ }
133
+ return { installed: true, label, plist: path, loaded: options.load !== false, logs: join(servePaths(root).state, 'logs') };
134
+ }
135
+ export async function uninstallLaunchd(root, options = {}) {
136
+ if ((options.platform ?? process.platform) !== 'darwin')
137
+ throw Error('kit_launchd_macos_only');
138
+ root = await realpath(root);
139
+ const label = launchdLabel(root), path = join(options.home ?? homedir(), 'Library', 'LaunchAgents', label + '.plist');
140
+ const exec = options.exec ?? promisify(execFile);
141
+ await exec('launchctl', ['bootout', 'gui/' + (options.uid ?? process.getuid?.() ?? 0) + '/' + label]).catch(() => { });
142
+ await unlink(path).catch(e => { if (e.code !== 'ENOENT')
143
+ throw e; });
144
+ return { uninstalled: true, label, plist: path };
145
+ }
@@ -0,0 +1,63 @@
1
+ import type { PortalLogin } from './onboarding.js';
2
+ import { type SetupPrompts } from './native-selection.js';
3
+ import { type NativeHarness } from './native.js';
4
+ export declare function nativeSdkConfig(harness: NativeHarness, root: string, options?: {
5
+ codexDelegationDisabled?: boolean;
6
+ }): {
7
+ config: {
8
+ features: {
9
+ multi_agent: boolean;
10
+ multi_agent_v2: boolean;
11
+ };
12
+ mcp_servers: {
13
+ tealbrick: {
14
+ command: string;
15
+ args: string[];
16
+ };
17
+ };
18
+ };
19
+ servers?: undefined;
20
+ settings?: undefined;
21
+ mcpServers?: undefined;
22
+ } | {
23
+ servers: {
24
+ tealbrick: {
25
+ command: string;
26
+ args: string[];
27
+ };
28
+ };
29
+ config?: undefined;
30
+ settings?: undefined;
31
+ mcpServers?: undefined;
32
+ } | {
33
+ settings: {
34
+ hooks: {
35
+ PreToolUse: {
36
+ matcher: string;
37
+ hooks: {
38
+ type: string;
39
+ command: string;
40
+ args: string[];
41
+ }[];
42
+ }[];
43
+ };
44
+ };
45
+ mcpServers: {
46
+ tealbrick: {
47
+ command: string;
48
+ args: string[];
49
+ };
50
+ };
51
+ config?: undefined;
52
+ servers?: undefined;
53
+ };
54
+ /** Private setup journal stores only identity intent, never account/runtime credentials. */
55
+ export declare function guidedNativeSetup(root: string, flags: Record<string, string>, login: PortalLogin, prompts: SetupPrompts): Promise<{
56
+ workspaceId: string;
57
+ nodeId: string;
58
+ configPath: string;
59
+ connected: boolean;
60
+ agentId: string;
61
+ connectionId: string;
62
+ harness: "claude" | "codex" | "langchain";
63
+ }>;