@tealbrick/kit 0.3.0-rc.6 → 0.3.0-rc.8

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.
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { lstat, readFile, realpath, writeFile, rename, mkdir } from 'node:fs/promises';
3
- import { isAbsolute, join, resolve } from 'node:path';
3
+ import { isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  /** Operator configuration for `tealbrick native serve`. Contains no credentials. */
6
6
  const absolute = z.string().min(1).refine(v => isAbsolute(v) && !v.includes('\0'), 'absolute path required');
@@ -16,6 +16,34 @@ export const defaultAccessTools = ['WebSearch', 'WebFetch', 'Agent', 'TodoWrite'
16
16
  /** Tools an expert subagent may hold; workspace reads stay contained and network content stays untrusted. */
17
17
  const expertTool = z.enum(['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch']);
18
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();
19
47
  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();
20
48
  const claude = z.object({
21
49
  /** Package directory or entry file of @anthropic-ai/claude-agent-sdk. Default: resolved from the kit root. */
@@ -34,6 +62,10 @@ const claude = z.object({
34
62
  defaultTools: z.array(z.enum(defaultAccessTools)).max(4).optional(),
35
63
  /** Expert subagents the Agent tool may delegate to, foreground only, inheriting the session model. */
36
64
  experts: z.record(expertName, expert).optional(),
65
+ /** Versioned expert contracts (JSON files) run as SDK subagents. Effective scope = contract ceiling ∩ owner grants. */
66
+ contracts: z.array(absolute).max(32).optional(),
67
+ /** Operations that always pause for the owner's approval of the exact payload, whoever calls them. */
68
+ approvalOperations: z.array(opName).max(64).optional(),
37
69
  /** Tool allowlist for the opt-in "Native defaults" session access mode. */
38
70
  nativeTools: z.array(tool).max(64).optional(),
39
71
  /** Operator-vetted read-only MCP tools, also available in Restricted mode. */
@@ -42,8 +74,10 @@ const claude = z.object({
42
74
  tealbrickCalls: z.array(tealbrickCallGrant).max(64).optional(),
43
75
  /** Refuse turns unless the SDK reports first-party subscription sign-in (no API-key fallback). */
44
76
  requireSubscription: z.boolean().optional(),
77
+ /** Opt-in sandboxed workspace for the owner session (inside the kit root, never containing .tealbrick). */
78
+ workspace: workspace.optional(),
45
79
  /** Extra environment variable names passed to the SDK process. TEALBRICK_* is never passed. */
46
- envPassthrough: z.array(z.string().regex(/^[A-Z][A-Z0-9_]{0,63}$/).refine(n => !n.startsWith('TEALBRICK_'), 'Teal Brick credentials are never passed to the harness')).max(32).optional(),
80
+ envPassthrough: z.array(envName.refine(n => !n.startsWith('TEALBRICK_'), 'Teal Brick credentials are never passed to the harness')).max(32).optional(),
47
81
  }).strict();
48
82
  const codex = z.object({ codexBin: absolute, cwd: absolute }).strict();
49
83
  export const serveConfigSchema = z.object({
@@ -59,7 +93,9 @@ export const serveConfigSchema = z.object({
59
93
  claude: claude.optional(),
60
94
  codex: codex.optional(),
61
95
  }).strict();
62
- export const servePaths = (root) => ({ config: join(root, '.tealbrick', 'native-serve.json'), state: join(root, '.tealbrick', 'native-serve'), sdk: join(root, '.tealbrick', 'native-sdk.json') });
96
+ export const servePaths = (root) => ({ config: join(root, '.tealbrick', 'native-serve.json'), state: join(root, '.tealbrick', 'native-serve'), sdk: join(root, '.tealbrick', 'native-sdk.json'),
97
+ /** Kit-owned attachment inbox: written only by the kit process, readable (never writable) by the sandboxed workspace session. */
98
+ inbox: join(root, 'inbox') });
63
99
  async function regularFile(path, { ownerOnly }) {
64
100
  const stat = await lstat(path);
65
101
  if (!stat.isFile() || stat.isSymbolicLink())
@@ -164,7 +200,8 @@ export async function resolveClaudeProfile(root, config) {
164
200
  if (agent.tealbrickSdkConfig && await realpath(agent.tealbrickSdkConfig) !== await realpath(servePaths(root).sdk))
165
201
  throw Error('kit_native_serve_recipe_connection_mismatch');
166
202
  }
167
- const cwd = resolve(c.cwd), protectedRoot = join(await realpath(root), '.tealbrick');
203
+ let cwd = resolve(c.cwd);
204
+ const protectedRoot = join(await realpath(root), '.tealbrick');
168
205
  if (protectedRoot.startsWith(cwd + '/') || cwd === protectedRoot || cwd.startsWith(protectedRoot + '/'))
169
206
  throw Error('kit_native_serve_cwd_contains_credentials');
170
207
  if (!(await lstat(cwd).catch(() => undefined))?.isDirectory())
@@ -172,7 +209,42 @@ export async function resolveClaudeProfile(root, config) {
172
209
  const instructions = c.instructionsFile ? (await regularFile(c.instructionsFile, { ownerOnly: false }), await readFile(c.instructionsFile, 'utf8')) : undefined;
173
210
  if (instructions && instructions.length > 200000)
174
211
  throw Error('kit_native_serve_instructions_too_large');
212
+ let workspace;
213
+ if (c.workspace) {
214
+ const realRoot = await realpath(root), inside = (base, p) => { const rel = relative(base, p); return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel); };
215
+ const dir = resolve(c.workspace.dir), tb = (base) => join(base, '.tealbrick');
216
+ const lexical = (base) => inside(base, dir) && dir !== tb(base) && !inside(tb(base), dir);
217
+ if (!lexical(realRoot) && !lexical(resolve(root)))
218
+ throw Error('kit_native_serve_workspace_outside_root');
219
+ // The attachment inbox must stay outside anything the sandbox can write.
220
+ const overlapsInbox = (base, p) => { const inbox = servePaths(base).inbox; return p === inbox || inside(inbox, p); };
221
+ if (overlapsInbox(realRoot, dir) || overlapsInbox(resolve(root), dir))
222
+ throw Error('kit_native_serve_workspace_overlaps_inbox');
223
+ await mkdir(dir, { recursive: true, mode: 0o700 });
224
+ const real = await realpath(dir);
225
+ if (!inside(realRoot, real) || real === tb(realRoot) || inside(tb(realRoot), real))
226
+ throw Error('kit_native_serve_workspace_outside_root');
227
+ if (overlapsInbox(realRoot, real))
228
+ throw Error('kit_native_serve_workspace_overlaps_inbox');
229
+ if (await lstat(join(real, '.tealbrick')).then(() => true, () => false))
230
+ throw Error('kit_native_serve_workspace_contains_credentials');
231
+ workspace = { dir: real, root: realRoot, allowedDomains: [...new Set(c.workspace.network?.allowedDomains ?? [])], envAllow: [...new Set(c.workspace.envAllow ?? [])] };
232
+ cwd = real;
233
+ }
175
234
  const readTools = c.readTools ?? agent?.readTools ?? [];
235
+ const contracts = {};
236
+ for (const path of c.contracts ?? []) {
237
+ await regularFile(path, { ownerOnly: false });
238
+ const text = await readFile(path, 'utf8');
239
+ if (text.length > 65536)
240
+ throw Error('kit_native_serve_contract_too_large');
241
+ const contract = parsed(expertContractSchema, text, 'kit_native_serve_contract_invalid');
242
+ if (contract.runtime !== 'subagent')
243
+ throw Error('kit_native_serve_contract_runtime_unsupported:' + contract.id);
244
+ if (Object.hasOwn(contracts, contract.id))
245
+ throw Error('kit_native_serve_contract_duplicate:' + contract.id);
246
+ contracts[contract.id] = contract;
247
+ }
176
248
  const experts = {};
177
249
  for (const [name, e] of Object.entries(c.experts ?? {})) {
178
250
  await regularFile(e.promptFile, { ownerOnly: false });
@@ -181,10 +253,13 @@ export async function resolveClaudeProfile(root, config) {
181
253
  throw Error('kit_native_serve_expert_prompt_invalid:' + name);
182
254
  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 } : {}) };
183
255
  }
256
+ for (const id of Object.keys(contracts))
257
+ if (Object.hasOwn(experts, id))
258
+ throw Error('kit_native_serve_contract_duplicate:' + id);
184
259
  return { cwd, sdkModule: c.sdkModule, instructions, model: c.model ?? recipe?.model, effort: c.effort ?? recipe?.effort,
185
260
  maxTurns: c.maxTurns ?? recipe?.maxTurns ?? 24, maxBudgetUsd: c.maxBudgetUsd ?? recipe?.maxBudgetUsd, timeoutSeconds: c.timeoutSeconds ?? recipe?.timeoutSeconds ?? 900,
186
- defaultTools: [...new Set(c.defaultTools ?? [])], experts,
261
+ defaultTools: [...new Set(c.defaultTools ?? [])], experts, contracts, approvalOperations: [...new Set(c.approvalOperations ?? ['marketplace_execute'])],
187
262
  nativeTools: c.nativeTools ?? ['Read', 'Glob', 'Grep', 'TodoWrite'], readTools: readTools.filter(t => !t.startsWith('mcp__tealbrick__')),
188
263
  mcpServers: c.mcpServers ?? agent?.mcpServers ?? {}, tealbrickCalls: c.tealbrickCalls ?? agent?.tealbrickCalls ?? [],
189
- requireSubscription: c.requireSubscription ?? false, envPassthrough: c.envPassthrough ?? [] };
264
+ requireSubscription: c.requireSubscription ?? false, envPassthrough: c.envPassthrough ?? [], ...(workspace ? { workspace } : {}) };
190
265
  }
@@ -13,6 +13,7 @@ export interface ServeDependencies {
13
13
  runtimeSync?: RuntimeSyncOptions | false;
14
14
  /** Test seams for the owner-only /tealbrick/v1/capabilities snapshot. */
15
15
  appCapabilities?: AppCapabilitiesOptions;
16
+ approvalTimeoutMs?: number;
16
17
  }
17
18
  /** Build (but do not bind) the owner-authenticated chat endpoint for this kit root. */
18
19
  export declare function createNativeServe(root: string, deps?: ServeDependencies): Promise<{
@@ -35,9 +35,20 @@ export async function createNativeServe(root, deps = {}) {
35
35
  await mkdir(paths.state, { recursive: true, mode: 0o700 });
36
36
  let adapter;
37
37
  let codex;
38
+ let runtime;
39
+ const appCapabilities = appCapabilitiesProvider(root, () => runtime, deps.appCapabilities);
38
40
  if (kit.native.harness === 'claude') {
39
41
  const profile = await resolveClaudeProfile(root, config);
40
- 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 });
42
+ // Contract scope checks resolve registration ids from this runtime's own Portal-granted snapshot.
43
+ const resolveApp = async (registrationId) => {
44
+ const snap = await appCapabilities();
45
+ const app = snap.apps.find(a => a.registrationId === registrationId);
46
+ if (app)
47
+ return { appId: app.appId };
48
+ const grant = snap.marketplace.grants.find(g => g.registrationId === registrationId);
49
+ return grant ? { appId: 'marketplace', plugin: grant.plugin, action: grant.action } : undefined;
50
+ };
51
+ 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'), ...(deps.approvalTimeoutMs ? { approvalTimeoutMs: deps.approvalTimeoutMs } : {}) });
41
52
  }
42
53
  else if (kit.native.harness === 'codex') {
43
54
  if (!config.codex)
@@ -50,8 +61,6 @@ export async function createNativeServe(root, deps = {}) {
50
61
  else
51
62
  adapter = langchainHarness();
52
63
  const authenticate = deps.authenticate ?? createPortalVerifier({ issuer: kit.runtime.issuer, org: kit.runtime.org, agent: config.agent.name }).authenticate;
53
- let runtime;
54
- const appCapabilities = appCapabilitiesProvider(root, () => runtime, deps.appCapabilities);
55
64
  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 });
56
65
  // Portal marks a runtime expired unless it re-acknowledges its signed config within the 300s lease;
57
66
  // an idle serve process has no per-turn MCP child doing that, so it keeps the lease itself.
package/dist/native.d.ts CHANGED
@@ -37,6 +37,6 @@ export declare function nativeConnector(): {
37
37
  }>;
38
38
  call: (registrationId: string, operation: KnowledgeOperation, input?: Record<string, unknown>, signal?: AbortSignal) => Promise<any>;
39
39
  };
40
- export declare const nativeUsage = "Usage: tealbrick native setup --harness claude|codex|langchain [--workspace-name NAME | --create-workspace NAME] [--agent-name NAME | --create-agent NAME] [--issuer HTTPS_ORIGIN] | native mcp [--root DIRECTORY] | native serve [--root DIRECTORY] [--install-launchd [--no-load] | --uninstall] | native enroll --url HTTPS_ORIGIN [--port PORT] [--root DIRECTORY] [--skip-endpoint-check] | native enable|disable | native marketplace-enable|marketplace-disable";
40
+ export declare const nativeUsage = "Usage: tealbrick native setup --harness claude|codex|langchain [--workspace-name NAME | --create-workspace NAME] [--agent-name NAME | --create-agent NAME] [--issuer HTTPS_ORIGIN] | native mcp [--root DIRECTORY] | native serve [--root DIRECTORY] [--install-launchd [--no-load] | --uninstall] | native enroll --url HTTPS_ORIGIN [--port PORT] [--root DIRECTORY] [--skip-endpoint-check] | native acp [--root DIRECTORY] [--buzz-cli PATH] | native enable|disable | native marketplace-enable|marketplace-disable";
41
41
  export declare function nativeCommand(args: string[], root: string): Promise<void>;
42
42
  export { guidedNativeSetup, nativeSdkConfig };
package/dist/native.js CHANGED
@@ -64,8 +64,18 @@ function serveFlags(args, values, switches) {
64
64
  }
65
65
  return flags;
66
66
  }
67
- export const nativeUsage = 'Usage: tealbrick native setup --harness claude|codex|langchain [--workspace-name NAME | --create-workspace NAME] [--agent-name NAME | --create-agent NAME] [--issuer HTTPS_ORIGIN] | native mcp [--root DIRECTORY] | native serve [--root DIRECTORY] [--install-launchd [--no-load] | --uninstall] | native enroll --url HTTPS_ORIGIN [--port PORT] [--root DIRECTORY] [--skip-endpoint-check] | native enable|disable | native marketplace-enable|marketplace-disable';
67
+ export const nativeUsage = 'Usage: tealbrick native setup --harness claude|codex|langchain [--workspace-name NAME | --create-workspace NAME] [--agent-name NAME | --create-agent NAME] [--issuer HTTPS_ORIGIN] | native mcp [--root DIRECTORY] | native serve [--root DIRECTORY] [--install-launchd [--no-load] | --uninstall] | native enroll --url HTTPS_ORIGIN [--port PORT] [--root DIRECTORY] [--skip-endpoint-check] | native acp [--root DIRECTORY] [--buzz-cli PATH] | native enable|disable | native marketplace-enable|marketplace-disable';
68
68
  export async function nativeCommand(args, root) {
69
+ if (args[0] === 'acp') {
70
+ const flags = serveFlags(args.slice(1), ['root', 'buzz-cli'], []);
71
+ if (typeof flags.root === 'string') {
72
+ root = resolve(flags.root);
73
+ process.chdir(root);
74
+ }
75
+ const { serveAcp } = await import('./native-acp.js');
76
+ await serveAcp(root, typeof flags['buzz-cli'] === 'string' ? { buzzCli: resolve(flags['buzz-cli']) } : {});
77
+ return;
78
+ }
69
79
  if (args[0] === 'serve' || args[0] === 'enroll') {
70
80
  const flags = args[0] === 'serve' ? serveFlags(args.slice(1), ['root'], ['install-launchd', 'uninstall', 'no-load']) : serveFlags(args.slice(1), ['root', 'url', 'port'], ['skip-endpoint-check']);
71
81
  if (typeof flags.root === 'string') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tealbrick/kit",
3
- "version": "0.3.0-rc.6",
3
+ "version": "0.3.0-rc.8",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Complete Teal Brick package kit with explicit Eve capability activation",
@@ -14,7 +14,9 @@
14
14
  "NOTICE",
15
15
  "BOOTSTRAP.md",
16
16
  "RUNTIME.md",
17
- "NATIVE.md"
17
+ "NATIVE.md",
18
+ "BUZZ.md",
19
+ "assets"
18
20
  ],
19
21
  "bin": {
20
22
  "tealbrick": "dist/cli.js"
@@ -43,11 +45,11 @@
43
45
  "test": "node --test test/*.test.mjs"
44
46
  },
45
47
  "dependencies": {
46
- "@tealbrick/portal": "0.3.0-rc.6",
47
- "@tealbrick/avm": "0.3.0-rc.6",
48
- "@tealbrick/voice": "0.3.0-rc.6",
49
- "@tealbrick/vision": "0.3.0-rc.6",
50
- "@tealbrick/deliver": "0.3.0-rc.6",
48
+ "@tealbrick/portal": "0.3.0-rc.8",
49
+ "@tealbrick/avm": "0.3.0-rc.8",
50
+ "@tealbrick/voice": "0.3.0-rc.8",
51
+ "@tealbrick/vision": "0.3.0-rc.8",
52
+ "@tealbrick/deliver": "0.3.0-rc.8",
51
53
  "@inquirer/prompts": "8.7.2",
52
54
  "zod": "^4.0.0"
53
55
  },