@tealbrick/kit 0.3.0-rc.3 → 0.3.0-rc.5

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.
@@ -0,0 +1,128 @@
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 { publicHosts, readClaudeAttachment, readServeConfig, resolveClaudeProfile, servePaths } from './native-serve-config.js';
16
+ /** LangChain has no stable session/stream/cancel contract to drive from here yet. */
17
+ export function langchainHarness() { throw Error('kit_native_serve_langchain_unsupported'); }
18
+ /** Build (but do not bind) the owner-authenticated chat endpoint for this kit root. */
19
+ export async function createNativeServe(root, deps = {}) {
20
+ root = await realpath(root);
21
+ const kit = await readKit(root);
22
+ if (!kit.native || !kit.runtime)
23
+ throw Error('kit_native_setup_required');
24
+ if (!kit.native.active)
25
+ throw Error('runtime_product_inactive');
26
+ const config = await readServeConfig(root);
27
+ if (!config)
28
+ throw Error('kit_native_serve_config_required');
29
+ if (!config.agent)
30
+ throw Error('kit_native_serve_enroll_required');
31
+ if (config.agent.id !== kit.runtime.agentId)
32
+ throw Error('kit_native_serve_identity_mismatch');
33
+ const paths = servePaths(root), log = deps.log;
34
+ await mkdir(paths.state, { recursive: true, mode: 0o700 });
35
+ let adapter;
36
+ let codex;
37
+ if (kit.native.harness === 'claude') {
38
+ const profile = await resolveClaudeProfile(root, config);
39
+ 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 });
40
+ }
41
+ else if (kit.native.harness === 'codex') {
42
+ if (!config.codex)
43
+ throw Error('kit_native_serve_codex_profile_required');
44
+ codex = new CodexAppServer(config.codex.codexBin, config.codex.cwd);
45
+ await codex.start();
46
+ const mcp = nativeSdkConfig('codex', root, { codexDelegationDisabled: true }).config.mcp_servers.tealbrick;
47
+ adapter = codexHarness(codex, { name: config.agent.name, cwd: config.codex.cwd, mcp, features: { multi_agent_v2: false }, log });
48
+ }
49
+ else
50
+ adapter = langchainHarness();
51
+ const authenticate = deps.authenticate ?? createPortalVerifier({ issuer: kit.runtime.issuer, org: kit.runtime.org, agent: config.agent.name }).authenticate;
52
+ 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 });
53
+ // Portal marks a runtime expired unless it re-acknowledges its signed config within the 300s lease;
54
+ // an idle serve process has no per-turn MCP child doing that, so it keeps the lease itself.
55
+ const runtime = deps.runtimeSync === false ? undefined : startRuntimeSync(root, { log, ...deps.runtimeSync });
56
+ return { ...bridge, port: config.port, harness: kit.native.harness, runtime, close: async () => { await runtime?.stop(); await bridge.close(); await codex?.close(); } };
57
+ }
58
+ export async function serveNative(root) {
59
+ const serve = await createNativeServe(root);
60
+ // Loopback only. Remote reachability is a TLS-terminating proxy such as tailscale serve.
61
+ await new Promise((resolve, reject) => { serve.server.once('error', reject); serve.server.listen(serve.port, '127.0.0.1', () => resolve()); });
62
+ process.stderr.write(JSON.stringify({ event: 'native.serve.ready', harness: serve.harness, port: serve.port }) + '\n');
63
+ for (const signal of ['SIGTERM', 'SIGINT'])
64
+ process.once(signal, () => { void serve.close().finally(() => process.exit(0)); });
65
+ }
66
+ const xml = (v) => v.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
67
+ export function launchdLabel(root) { return 'com.tealbrick.native-serve.' + createHash('sha256').update(root).digest('hex').slice(0, 12); }
68
+ /** User LaunchAgent: restarts on crash, logs to the kit root's private state directory. */
69
+ export function launchdPlist(root, options = {}) {
70
+ const node = options.node ?? process.execPath, cli = options.cli ?? fileURLToPath(new URL('./cli.js', import.meta.url)), logs = join(servePaths(root).state, 'logs');
71
+ const args = [node, cli, 'native', 'serve', '--root', root];
72
+ return `<?xml version="1.0" encoding="UTF-8"?>
73
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
74
+ <plist version="1.0">
75
+ <dict>
76
+ <key>Label</key><string>${xml(launchdLabel(root))}</string>
77
+ <key>ProgramArguments</key><array>${args.map(a => `<string>${xml(a)}</string>`).join('')}</array>
78
+ <key>WorkingDirectory</key><string>${xml(root)}</string>
79
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(dirname(node))}:/usr/bin:/bin:/usr/sbin:/sbin</string></dict>
80
+ <key>RunAtLoad</key><true/>
81
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
82
+ <key>ThrottleInterval</key><integer>30</integer>
83
+ <key>ProcessType</key><string>Background</string>
84
+ <key>Umask</key><integer>63</integer>
85
+ <key>StandardOutPath</key><string>${xml(join(logs, 'serve.out.log'))}</string>
86
+ <key>StandardErrorPath</key><string>${xml(join(logs, 'serve.err.log'))}</string>
87
+ </dict>
88
+ </plist>
89
+ `;
90
+ }
91
+ export async function installLaunchd(root, options = {}) {
92
+ if ((options.platform ?? process.platform) !== 'darwin')
93
+ throw Error('kit_launchd_macos_only');
94
+ root = await realpath(root);
95
+ const config = await readServeConfig(root);
96
+ if (!config?.agent)
97
+ throw Error('kit_native_serve_enroll_required');
98
+ const label = launchdLabel(root), dir = join(options.home ?? homedir(), 'Library', 'LaunchAgents'), path = join(dir, label + '.plist');
99
+ await mkdir(join(servePaths(root).state, 'logs'), { recursive: true, mode: 0o700 });
100
+ await mkdir(dir, { recursive: true });
101
+ try {
102
+ if ((await lstat(path)).isSymbolicLink())
103
+ throw Error('kit_symlink_path');
104
+ }
105
+ catch (e) {
106
+ if (e.code !== 'ENOENT')
107
+ throw e;
108
+ }
109
+ await writeFile(path + '.tmp', launchdPlist(root), { mode: 0o600 });
110
+ await rename(path + '.tmp', path);
111
+ const exec = options.exec ?? promisify(execFile), domain = 'gui/' + (options.uid ?? process.getuid?.() ?? 0);
112
+ if (options.load !== false) {
113
+ await exec('launchctl', ['bootout', domain + '/' + label]).catch(() => { });
114
+ await exec('launchctl', ['bootstrap', domain, path]);
115
+ }
116
+ return { installed: true, label, plist: path, loaded: options.load !== false, logs: join(servePaths(root).state, 'logs') };
117
+ }
118
+ export async function uninstallLaunchd(root, options = {}) {
119
+ if ((options.platform ?? process.platform) !== 'darwin')
120
+ throw Error('kit_launchd_macos_only');
121
+ root = await realpath(root);
122
+ const label = launchdLabel(root), path = join(options.home ?? homedir(), 'Library', 'LaunchAgents', label + '.plist');
123
+ const exec = options.exec ?? promisify(execFile);
124
+ await exec('launchctl', ['bootout', 'gui/' + (options.uid ?? process.getuid?.() ?? 0) + '/' + label]).catch(() => { });
125
+ await unlink(path).catch(e => { if (e.code !== 'ENOENT')
126
+ throw e; });
127
+ return { uninstalled: true, label, plist: path };
128
+ }
package/dist/native.js CHANGED
@@ -47,7 +47,69 @@ export function nativeConnector() {
47
47
  return { marketplace: async () => { const config = await readKit(process.cwd()); if (!config.native?.active || !config.native.marketplace || !config.runtime)
48
48
  return undefined; const { credentialRef, ...identity } = config.runtime; return new MarketplaceRuntimeConnector({ ...identity, credential: () => runtimeSecret(credentialRef) }); }, capabilities: async () => (await current()).capabilities(true), call: async (registrationId, operation, input = {}, signal) => (await current()).call(registrationId, operation, input, signal) };
49
49
  }
50
+ /** serve/enroll take boolean switches, so they are parsed separately from setup flags. */
51
+ function serveFlags(args, values, switches) {
52
+ const flags = {};
53
+ for (let i = 0; i < args.length; i++) {
54
+ const name = /^--([a-z-]+)$/.exec(args[i])?.[1];
55
+ if (!name || flags[name] !== undefined)
56
+ throw Error('kit_native_invalid_arguments');
57
+ if (switches.includes(name)) {
58
+ flags[name] = true;
59
+ continue;
60
+ }
61
+ if (!values.includes(name) || !args[i + 1] || args[i + 1].startsWith('--'))
62
+ throw Error('kit_native_invalid_arguments');
63
+ flags[name] = args[++i];
64
+ }
65
+ return flags;
66
+ }
50
67
  export async function nativeCommand(args, root) {
68
+ if (args[0] === 'serve' || args[0] === 'enroll') {
69
+ 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']);
70
+ if (typeof flags.root === 'string') {
71
+ root = resolve(flags.root);
72
+ process.chdir(root);
73
+ }
74
+ if (args[0] === 'serve') {
75
+ const { serveNative, installLaunchd, uninstallLaunchd } = await import('./native-serve.js');
76
+ if (flags['install-launchd'] && flags.uninstall)
77
+ throw Error('kit_native_invalid_arguments');
78
+ if (flags.uninstall) {
79
+ console.log(JSON.stringify(await uninstallLaunchd(root)));
80
+ return;
81
+ }
82
+ if (flags['install-launchd']) {
83
+ console.log(JSON.stringify(await installLaunchd(root, { load: !flags['no-load'] })));
84
+ return;
85
+ }
86
+ if (flags['no-load'])
87
+ throw Error('kit_native_invalid_arguments');
88
+ await serveNative(root);
89
+ return;
90
+ }
91
+ if (typeof flags.url !== 'string')
92
+ throw Error('kit_native_public_url_required');
93
+ const { enrollNative } = await import('./native-enroll.js');
94
+ const state = await readKit(root);
95
+ if (!state.native || !state.runtime)
96
+ throw Error('kit_native_setup_required');
97
+ const login = await loginPortal(portalOrigin(state.runtime.issuer), (url, code) => console.error(`Approve Tealbrick endpoint registration: ${url}\nCode: ${code}`));
98
+ try {
99
+ const result = await enrollNative(root, { url: flags.url, ...(typeof flags.port === 'string' ? { port: Number(flags.port) } : {}) }, login, { verifyEndpoint: !flags['skip-endpoint-check'] });
100
+ console.log(JSON.stringify(result));
101
+ if (!flags['skip-endpoint-check'] && !result.endpointVerified) {
102
+ console.error('Portal registration is saved. Start `tealbrick native serve` and the HTTPS proxy, then re-run enroll to verify the endpoint.');
103
+ process.exitCode = 2;
104
+ }
105
+ }
106
+ catch (error) {
107
+ if (error instanceof PortalHttpError && /^[a-z_]+$/.test(error.code ?? ''))
108
+ throw Error('kit_portal_' + error.code);
109
+ throw error;
110
+ }
111
+ return;
112
+ }
51
113
  const command = args[0], flags = {};
52
114
  for (let i = 1; i < args.length; i += 2) {
53
115
  if (!/^--(root|harness|workspace|workspace-name|create-workspace|node|agent|agent-name|create-agent|delegation|issuer)$/.test(args[i]) || !args[i + 1] || flags[args[i].slice(2)])
@@ -109,6 +171,6 @@ export async function nativeCommand(args, root) {
109
171
  console.log(JSON.stringify({ active: command === 'enable' }));
110
172
  return;
111
173
  }
112
- throw Error('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 enable|disable | native marketplace-enable|marketplace-disable');
174
+ throw Error('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');
113
175
  }
114
176
  export { guidedNativeSetup, nativeSdkConfig };
@@ -8,6 +8,11 @@ export interface PortalLogin {
8
8
  id: string;
9
9
  name: string;
10
10
  }>;
11
+ /** Ephemeral user identity for endpoint verification. Never persisted. */
12
+ identity?: {
13
+ token: string;
14
+ bundle: string;
15
+ };
11
16
  }
12
17
  /** Reject URLs that would leak credentials or be silently truncated by registration. */
13
18
  export declare function portalOrigin(value: string): string;
@@ -53,7 +53,7 @@ export async function loginPortal(issuer, show, options = {}) {
53
53
  const directory = await authenticated.request('/api/me/workspaces');
54
54
  if (!account.user?.id || !account.user.orgId || directory.accountId !== account.user.id || directory.organizationId !== account.user.orgId || !Array.isArray(directory.workspaces) || directory.workspaces.some(w => !w.id || !w.name))
55
55
  throw Error('kit_portal_account_mismatch');
56
- return { issuer, client: authenticated, org: account.user.orgId, userId: account.user.id, workspaces: directory.workspaces };
56
+ return { issuer, client: authenticated, org: account.user.orgId, userId: account.user.id, workspaces: directory.workspaces, identity: { token: session.token, bundle: session.bundle } };
57
57
  }
58
58
  /** Reuse Portal's owned registration + revision-checked canvas APIs, not a parallel card store. */
59
59
  export async function registerPortalCard(login, input) {
package/dist/secrets.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export declare function runtimeSecret(name: string): string;
1
+ /** Reads from the environment, then the kit root's private credentials file (cwd unless `root` is given). */
2
+ export declare function runtimeSecret(name: string, root?: string): string;
2
3
  /** Lazy: does not read credentials during discovery/build or for unused providers. */
3
4
  export declare function secretEnvironment(): NodeJS.ProcessEnv;
package/dist/secrets.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { readFileSync, lstatSync } from 'node:fs';
2
+ import { join } from 'node:path';
2
3
  const namePattern = /^TEALBRICK_[A-Z0-9_]+$/;
3
- export function runtimeSecret(name) {
4
+ /** Reads from the environment, then the kit root's private credentials file (cwd unless `root` is given). */
5
+ export function runtimeSecret(name, root) {
4
6
  if (!namePattern.test(name))
5
7
  throw Error('kit_invalid_secret_reference');
6
8
  let value = process.env[name];
7
9
  if (!value) {
8
10
  try {
9
- const path = '.tealbrick/kit.credentials.json', stat = lstatSync(path);
11
+ const path = root === undefined ? '.tealbrick/kit.credentials.json' : join(root, '.tealbrick', 'kit.credentials.json'), stat = lstatSync(path);
10
12
  if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== 'win32' && (stat.mode & 0o077)))
11
13
  throw Error('kit_secret_permissions');
12
14
  const data = JSON.parse(readFileSync(path, 'utf8'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tealbrick/kit",
3
- "version": "0.3.0-rc.3",
3
+ "version": "0.3.0-rc.5",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Complete Teal Brick package kit with explicit Eve capability activation",
@@ -43,11 +43,11 @@
43
43
  "test": "node --test test/*.test.mjs"
44
44
  },
45
45
  "dependencies": {
46
- "@tealbrick/portal": "0.3.0-rc.3",
47
- "@tealbrick/avm": "0.3.0-rc.3",
48
- "@tealbrick/voice": "0.3.0-rc.3",
49
- "@tealbrick/vision": "0.3.0-rc.3",
50
- "@tealbrick/deliver": "0.3.0-rc.3",
46
+ "@tealbrick/portal": "0.3.0-rc.5",
47
+ "@tealbrick/avm": "0.3.0-rc.5",
48
+ "@tealbrick/voice": "0.3.0-rc.5",
49
+ "@tealbrick/vision": "0.3.0-rc.5",
50
+ "@tealbrick/deliver": "0.3.0-rc.5",
51
51
  "@inquirer/prompts": "8.7.2",
52
52
  "zod": "^4.0.0"
53
53
  },