@tealbrick/kit 0.1.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/dist/index.js ADDED
@@ -0,0 +1,180 @@
1
+ import { z } from 'zod';
2
+ import { avmConfigSchema } from '@tealbrick/avm/client';
3
+ import { readFile, writeFile, mkdir, unlink, lstat, realpath, rename } from 'node:fs/promises';
4
+ import { resolve, dirname, relative, sep } from 'node:path';
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import { validateVoiceConfig } from '@tealbrick/voice';
7
+ import { validateVisionConfig } from '@tealbrick/vision';
8
+ import { validateDeliveryConfig } from '@tealbrick/deliver';
9
+ const auth = z.object({ issuer: z.string().url().refine(v => new URL(v).protocol === 'https:'), org: z.string().min(1), agent: z.string().min(1) }).strict();
10
+ const media = z.object({ settings: z.unknown(), authorizationModule: z.string().optional() }).strict();
11
+ const schema = z.object({ version: z.literal(1), portal: auth.optional(), voice: media.optional(), vision: media.optional(), deliver: z.unknown().optional(), avm: z.custom(v => avmConfigSchema.safeParse(v).success).transform(v => 'target' in v ? { target: v.target, sudo: v.sudo ?? false, ...Object.fromEntries(Object.entries(v).filter(([k]) => k !== 'target' && k !== 'sudo')) } : v).optional() }).strict();
12
+ export const capabilities = ['portal', 'voice', 'vision', 'deliver', 'avm'];
13
+ const marker = '// Managed by @tealbrick/kit. Change selection with tealbrick setup.\n';
14
+ const render = (body) => marker + body + '\n';
15
+ const hash = (data) => createHash('sha256').update(data).digest('hex');
16
+ /** Pure mount plan: dependencies alone never contribute capabilities to Eve. */
17
+ export function planKit(value) {
18
+ const c = schema.parse(value), files = {};
19
+ if ((c.voice || c.vision) && !c.portal)
20
+ throw Error('kit_media_requires_portal_binding');
21
+ if (c.portal)
22
+ files['agent/channels/eve.ts'] = render(`import {eveChannel} from 'eve/channels/eve';\nimport {portalAuth} from '@tealbrick/portal/eve';\nexport default eveChannel({auth:[portalAuth(${JSON.stringify(c.portal)})]});`);
23
+ for (const kind of ['voice', 'vision']) {
24
+ const item = c[kind];
25
+ if (!item)
26
+ continue;
27
+ const settings = kind === 'voice' ? validateVoiceConfig(item.settings) : validateVisionConfig(item.settings);
28
+ if (!settings.enabled)
29
+ throw Error('kit_selected_media_must_be_enabled');
30
+ const endpoints = kind === 'vision' ? [settings.endpoint] : Object.values(settings).filter((v) => !!v && typeof v === 'object' && 'url' in v);
31
+ if (!endpoints.length)
32
+ throw Error('kit_selected_media_requires_endpoint');
33
+ const approved = new Map();
34
+ for (const e of endpoints) {
35
+ if (e.credentialRef && !/^TEALBRICK_[A-Z0-9_]+$/.test(e.credentialRef))
36
+ throw Error('kit_use_tealbrick_secret_environment_reference');
37
+ const previous = approved.get(e.url) ?? { url: e.url, credentialRefs: [], allowAnonymous: false, allowLoopback: new URL(e.url).protocol === 'http:' };
38
+ if (e.credentialRef)
39
+ previous.credentialRefs.push(e.credentialRef);
40
+ else
41
+ previous.allowAnonymous = true;
42
+ approved.set(e.url, previous);
43
+ }
44
+ const options = `{auth:${JSON.stringify(c.portal)},endpoints:${JSON.stringify([...approved.values()])},getConfig:()=>(${JSON.stringify(settings)}),resolveCredential:({reference})=>runtimeSecret(reference)}`;
45
+ files[`agent/channels/tealbrick-${kind}.ts`] = render(`import {${kind}Channel} from '@tealbrick/${kind}/eve';\nimport {runtimeSecret} from '@tealbrick/kit/secrets';\nexport default ${kind}Channel(${options});`);
46
+ if (kind === 'vision' && item.authorizationModule) {
47
+ const module = item.authorizationModule;
48
+ if (!/^agent\/lib\/[a-zA-Z0-9_/-]+\.(ts|js|mjs)$/.test(module) || module.includes('..'))
49
+ throw Error('kit_invalid_vision_authorization_module');
50
+ let specifier = relative('agent/tools', module).split(sep).join('/');
51
+ if (!specifier.startsWith('.'))
52
+ specifier = './' + specifier;
53
+ files['agent/tools/vision.ts'] = render(`import {visionTool} from '@tealbrick/vision/eve';\nimport {runtimeSecret} from '@tealbrick/kit/secrets';\nimport getAuthorization from ${JSON.stringify(specifier)};\nexport default visionTool({...${options},getAuthorization});`);
54
+ }
55
+ }
56
+ if (c.deliver !== undefined) {
57
+ const config = validateDeliveryConfig(c.deliver);
58
+ files['agent/tools/deliver.ts'] = render(`import {deliverTool} from '@tealbrick/deliver/eve';\nimport {secretEnvironment} from '@tealbrick/kit/secrets';\nexport default deliverTool({config:${JSON.stringify(config)},driverOptions:{env:secretEnvironment()}});`);
59
+ }
60
+ if (c.avm)
61
+ files['agent/extensions/tealbrick-avm.ts'] = render(`import avm from '@tealbrick/avm/eve';\nexport default avm(${JSON.stringify(c.avm)});`);
62
+ return { enabled: capabilities.filter(k => c[k] !== undefined), files };
63
+ }
64
+ async function optional(path) { try {
65
+ return await readFile(path, 'utf8');
66
+ }
67
+ catch (e) {
68
+ if (e.code === 'ENOENT')
69
+ return null;
70
+ throw e;
71
+ } }
72
+ /** Reject symlinked output parents; never write through project-controlled indirection. */
73
+ async function safe(root, path) { const resolved = resolve(root, path); if (!resolved.startsWith(root + sep))
74
+ throw Error('kit_invalid_path'); let current = root; for (const part of relative(root, resolved).split(sep)) {
75
+ current = resolve(current, part);
76
+ try {
77
+ if ((await lstat(current)).isSymbolicLink())
78
+ throw Error('kit_symlink_path');
79
+ }
80
+ catch (e) {
81
+ if (e.code !== 'ENOENT')
82
+ throw e;
83
+ }
84
+ } return resolved; }
85
+ export async function readKit(root) { const raw = await optional(resolve(root, '.tealbrick/kit-state.json')); if (!raw)
86
+ return { version: 1 }; const s = JSON.parse(raw); planKit(s.config); return s.config; }
87
+ /** Applies only exact kit-owned files. Unselected features have no mounts at all. */
88
+ export async function applyKit(root, config, credentials = {}) {
89
+ if (Object.entries(credentials).some(([k, v]) => !/^TEALBRICK_[A-Z0-9_]+$/.test(k) || typeof v !== 'string' || !v || /[\r\n\0]/.test(v)))
90
+ throw Error('kit_invalid_credentials');
91
+ root = await realpath(root);
92
+ const plan = planKit(config), statePath = await safe(root, '.tealbrick/kit-state.json');
93
+ await mkdir(dirname(statePath), { recursive: true, mode: 0o700 });
94
+ const lock = await safe(root, '.tealbrick/kit.lock');
95
+ try {
96
+ await writeFile(lock, 'activation\n', { flag: 'wx', mode: 0o600 });
97
+ }
98
+ catch (e) {
99
+ if (e.code === 'EEXIST')
100
+ throw Error('kit_activation_locked');
101
+ throw e;
102
+ }
103
+ try {
104
+ const raw = await optional(statePath), old = raw ? JSON.parse(raw) : { version: 1, config: { version: 1 }, files: {} };
105
+ if (old.version !== 1 || !old.files || !old.config)
106
+ throw Error('kit_invalid_state');
107
+ const oldPlan = planKit(old.config);
108
+ if (JSON.stringify(Object.keys(old.files).sort()) !== JSON.stringify(Object.keys(oldPlan.files).sort()))
109
+ throw Error('kit_invalid_state');
110
+ for (const [name, digest] of Object.entries(old.files))
111
+ if (digest !== hash(oldPlan.files[name]))
112
+ throw Error('kit_invalid_state');
113
+ const names = [...new Set([...Object.keys(old.files), ...Object.keys(plan.files)])];
114
+ const before = new Map();
115
+ for (const name of names) {
116
+ const path = await safe(root, name), content = await optional(path);
117
+ before.set(name, content);
118
+ if (name in old.files) {
119
+ if (content === null || hash(content) !== old.files[name])
120
+ throw Error(`kit_owned_file_changed:${name}`);
121
+ }
122
+ else if (content !== null)
123
+ throw Error(`kit_mount_conflict:${name}`);
124
+ }
125
+ if (config.vision?.authorizationModule) {
126
+ const path = await safe(root, config.vision.authorizationModule);
127
+ if (await optional(path) === null)
128
+ throw Error('kit_vision_authorization_module_missing');
129
+ }
130
+ const secretPath = await safe(root, '.tealbrick/kit.credentials.json'), secretBefore = await optional(secretPath);
131
+ if (secretBefore !== null && process.platform !== 'win32' && ((await lstat(secretPath)).mode & 0o077))
132
+ throw Error('kit_secret_permissions');
133
+ const mergedSecrets = { ...(secretBefore ? JSON.parse(secretBefore) : {}), ...credentials };
134
+ if (Object.entries(mergedSecrets).some(([k, v]) => !/^TEALBRICK_[A-Z0-9_]+$/.test(k) || typeof v !== 'string' || !v || /[\r\n\0]/.test(v)))
135
+ throw Error('kit_invalid_credentials');
136
+ const ignore = await safe(root, '.gitignore'), ignoreBefore = await optional(ignore);
137
+ const temp = `${statePath}.${randomUUID()}`;
138
+ try {
139
+ if (!(ignoreBefore ?? '').split(/\r?\n/).includes('/.tealbrick/'))
140
+ await writeFile(ignore, `${ignoreBefore ?? ''}${ignoreBefore && !ignoreBefore.endsWith('\n') ? '\n' : ''}/.tealbrick/\n`);
141
+ if (Object.keys(credentials).length)
142
+ await writeFile(secretPath, JSON.stringify(mergedSecrets, null, 2) + '\n', { mode: 0o600 });
143
+ for (const name of names) {
144
+ const path = resolve(root, name);
145
+ if (name in plan.files) {
146
+ await mkdir(dirname(path), { recursive: true });
147
+ await writeFile(path, plan.files[name]);
148
+ }
149
+ else
150
+ await unlink(path);
151
+ }
152
+ const state = { version: 1, config, files: Object.fromEntries(Object.entries(plan.files).map(([k, v]) => [k, hash(v)])) };
153
+ await writeFile(temp, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
154
+ await rename(temp, statePath);
155
+ }
156
+ catch (error) {
157
+ await unlink(temp).catch(() => { });
158
+ if (secretBefore === null)
159
+ await unlink(secretPath).catch(() => { });
160
+ else
161
+ await writeFile(secretPath, secretBefore, { mode: 0o600 });
162
+ for (const [name, content] of before) {
163
+ const path = resolve(root, name);
164
+ if (content === null)
165
+ await unlink(path).catch(() => { });
166
+ else
167
+ await writeFile(path, content);
168
+ }
169
+ if (ignoreBefore === null)
170
+ await unlink(ignore).catch(() => { });
171
+ else
172
+ await writeFile(ignore, ignoreBefore);
173
+ throw error;
174
+ }
175
+ return { enabled: plan.enabled, mounted: Object.keys(plan.files), removed: Object.keys(old.files).filter(k => !(k in plan.files)), requiresReload: true };
176
+ }
177
+ finally {
178
+ await unlink(lock);
179
+ }
180
+ }
@@ -0,0 +1,32 @@
1
+ import { PortalClient } from '@tealbrick/portal';
2
+ export interface PortalLogin {
3
+ client: PortalClient;
4
+ org: string;
5
+ userId: string;
6
+ workspaces: Array<{
7
+ id: string;
8
+ name: string;
9
+ }>;
10
+ }
11
+ /** Reject URLs that would leak credentials or be silently truncated by registration. */
12
+ export declare function portalOrigin(value: string): string;
13
+ export declare function agentOrigin(value: string): string;
14
+ /** Device authorization is interactive; user/refresh tokens never enter kit state. */
15
+ export declare function loginPortal(issuer: string, show: (url: string, code: string) => void, options?: {
16
+ fetch?: typeof fetch;
17
+ sleep?: (ms: number) => Promise<void>;
18
+ now?: () => number;
19
+ }): Promise<PortalLogin>;
20
+ /** Reuse Portal's owned registration + revision-checked canvas APIs, not a parallel card store. */
21
+ export declare function registerPortalCard(login: PortalLogin, input: {
22
+ workspaceId: string;
23
+ name: string;
24
+ url: string;
25
+ }): Promise<{
26
+ agentId: string;
27
+ workspaceId: string;
28
+ name: string;
29
+ url: string;
30
+ created: boolean;
31
+ discovered: boolean;
32
+ }>;
@@ -0,0 +1,105 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { PortalClient, PortalHttpError } from '@tealbrick/portal';
3
+ /** Reject URLs that would leak credentials or be silently truncated by registration. */
4
+ export function portalOrigin(value) {
5
+ const url = new URL(value);
6
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/')
7
+ throw Error('kit_portal_https_origin_required');
8
+ return url.origin;
9
+ }
10
+ export function agentOrigin(value) {
11
+ const url = new URL(value);
12
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/')
13
+ throw Error('kit_agent_origin_required');
14
+ return url.origin;
15
+ }
16
+ /** Device authorization is interactive; user/refresh tokens never enter kit state. */
17
+ export async function loginPortal(issuer, show, options = {}) {
18
+ issuer = portalOrigin(issuer);
19
+ const client = new PortalClient({ issuer, fetch: options.fetch });
20
+ const device = await client.startDevice({ deviceId: randomUUID(), deviceLabel: 'Teal Brick package setup' });
21
+ const verification = new URL(device.verification_uri_complete);
22
+ if (verification.origin !== issuer || verification.username || verification.password)
23
+ throw Error('kit_portal_verification_origin_mismatch');
24
+ show(verification.href, device.user_code);
25
+ const now = options.now ?? Date.now, sleep = options.sleep ?? (ms => new Promise(r => setTimeout(r, ms)));
26
+ const deadline = now() + Math.min(device.expires_in, 900) * 1000;
27
+ let interval = Math.max(1, device.interval) * 1000;
28
+ let session;
29
+ while (now() < deadline) {
30
+ await sleep(Math.min(interval, Math.max(0, deadline - now())));
31
+ if (now() >= deadline)
32
+ break;
33
+ try {
34
+ session = await client.pollDevice(device.device_code);
35
+ break;
36
+ }
37
+ catch (error) {
38
+ if (error instanceof PortalHttpError && error.code === 'authorization_pending')
39
+ continue;
40
+ if (error instanceof PortalHttpError && error.code === 'slow_down') {
41
+ interval += 5000;
42
+ continue;
43
+ }
44
+ throw error;
45
+ }
46
+ }
47
+ if (!session)
48
+ throw Error('kit_portal_login_expired');
49
+ if (session.issuer !== issuer)
50
+ throw Error('kit_portal_issuer_mismatch');
51
+ const authenticated = new PortalClient({ issuer, fetch: options.fetch, token: session.token });
52
+ const account = await authenticated.request('/api/account');
53
+ const directory = await authenticated.request('/api/me/workspaces');
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
+ throw Error('kit_portal_account_mismatch');
56
+ return { client: authenticated, org: account.user.orgId, userId: account.user.id, workspaces: directory.workspaces };
57
+ }
58
+ /** Reuse Portal's owned registration + revision-checked canvas APIs, not a parallel card store. */
59
+ export async function registerPortalCard(login, input) {
60
+ const { client } = login, url = agentOrigin(input.url), name = input.name.trim();
61
+ if (!name || name.length > 80)
62
+ throw Error('kit_agent_name_required');
63
+ if (!login.workspaces.some(w => w.id === input.workspaceId))
64
+ throw Error('kit_portal_workspace_required');
65
+ const path = `/api/workspace?workspaceId=${encodeURIComponent(input.workspaceId)}`;
66
+ const initial = await client.request(path);
67
+ if (initial.workspace?.id !== input.workspaceId || !Array.isArray(initial.agents))
68
+ throw Error('kit_portal_workspace_mismatch');
69
+ const existing = initial.agents?.find(a => a.name.toLowerCase() === name.toLowerCase());
70
+ if (existing && existing.name !== name)
71
+ throw Error('kit_portal_use_existing_agent_name');
72
+ if (existing && !existing.bindable)
73
+ throw Error('kit_portal_agent_not_owned');
74
+ // The endpoint is an upsert scoped by the authenticated organization and name.
75
+ const registered = await client.registerAgent(name, url);
76
+ if (registered.url !== url || registered.name !== name)
77
+ throw Error('kit_portal_registered_identity_mismatch');
78
+ for (let attempt = 0; attempt < 3; attempt++) {
79
+ const snapshot = await client.request(path);
80
+ if (snapshot.workspace?.id !== input.workspaceId || !Number.isInteger(snapshot.draftRevision) || !snapshot.agents?.some(a => a.id === registered.id && a.bindable))
81
+ throw Error('kit_portal_owned_agent_missing');
82
+ const draft = snapshot.draft ?? { version: 1, name: snapshot.workspace.name, nodes: [], edges: [] };
83
+ const matches = draft.nodes.filter(n => n.data.kind === 'agent' && n.data.agentId === registered.id);
84
+ if (matches.length > 1)
85
+ throw Error('kit_portal_duplicate_agent_card');
86
+ const previous = matches[0];
87
+ const card = { id: previous?.id ?? `tealbrick-${randomUUID()}`, type: 'instrument', position: previous?.position ?? { x: 80, y: 80 + draft.nodes.length * 120 }, data: { ...previous?.data, kind: 'agent', label: name, harness: 'eve', agentId: registered.id, endpoint: url, authMethod: 'portal-pairing' } };
88
+ if (!previous || previous.data.endpoint !== url || previous.data.label !== name || previous.data.harness !== 'eve' || previous.data.authMethod !== 'portal-pairing') {
89
+ const nodes = previous ? draft.nodes.map(n => n.id === previous.id ? card : n) : [...draft.nodes, card];
90
+ try {
91
+ await client.request('/api/workspace/draft', { workspaceId: input.workspaceId, draftRevision: snapshot.draftRevision, draft: { ...draft, nodes } });
92
+ }
93
+ catch (error) {
94
+ if (error instanceof PortalHttpError && error.code === 'draft_changed_reload' && attempt < 2)
95
+ continue;
96
+ throw error;
97
+ }
98
+ }
99
+ const directory = await client.request(`/api/me/agents?workspaceId=${encodeURIComponent(input.workspaceId)}`);
100
+ if (directory.workspaceId !== input.workspaceId || !directory.agents.some(a => a.id === registered.id && a.url === url))
101
+ throw Error('kit_portal_sidebar_verification_failed');
102
+ return { agentId: registered.id, workspaceId: input.workspaceId, name, url, created: registered.created, discovered: true };
103
+ }
104
+ throw Error('kit_portal_canvas_changed_retry');
105
+ }
@@ -0,0 +1,3 @@
1
+ export declare function runtimeSecret(name: string): string;
2
+ /** Lazy: does not read credentials during discovery/build or for unused providers. */
3
+ export declare function secretEnvironment(): NodeJS.ProcessEnv;
@@ -0,0 +1,25 @@
1
+ import { readFileSync, lstatSync } from 'node:fs';
2
+ const namePattern = /^TEALBRICK_[A-Z0-9_]+$/;
3
+ export function runtimeSecret(name) {
4
+ if (!namePattern.test(name))
5
+ throw Error('kit_invalid_secret_reference');
6
+ let value = process.env[name];
7
+ if (!value) {
8
+ try {
9
+ const path = '.tealbrick/kit.credentials.json', stat = lstatSync(path);
10
+ if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== 'win32' && (stat.mode & 0o077)))
11
+ throw Error('kit_secret_permissions');
12
+ const data = JSON.parse(readFileSync(path, 'utf8'));
13
+ value = data[name];
14
+ }
15
+ catch (e) {
16
+ if (e.code !== 'ENOENT')
17
+ throw Error('kit_secret_unavailable');
18
+ }
19
+ }
20
+ if (typeof value !== 'string' || !value || /[\r\n\0]/.test(value))
21
+ throw Error('kit_secret_unavailable');
22
+ return value;
23
+ }
24
+ /** Lazy: does not read credentials during discovery/build or for unused providers. */
25
+ export function secretEnvironment() { return new Proxy({}, { get: (_target, name) => typeof name === 'string' ? runtimeSecret(name) : undefined }); }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@tealbrick/kit",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Complete Teal Brick package kit with explicit Eve capability activation",
7
+ "engines": {
8
+ "node": ">=24"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE",
14
+ "NOTICE",
15
+ "BOOTSTRAP.md"
16
+ ],
17
+ "bin": {
18
+ "tealbrick": "dist/cli.js"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./secrets": {
26
+ "types": "./dist/secrets.d.ts",
27
+ "import": "./dist/secrets.js"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "build": "npm run build --workspace @tealbrick/provider-transport --workspace @tealbrick/portal --workspace @tealbrick/voice --workspace @tealbrick/vision --workspace @tealbrick/deliver --workspace @tealbrick/avm && tsc -p tsconfig.json",
32
+ "test": "node --test test/*.test.mjs"
33
+ },
34
+ "dependencies": {
35
+ "@tealbrick/portal": "0.1.0",
36
+ "@tealbrick/avm": "0.1.0",
37
+ "@tealbrick/voice": "0.1.0",
38
+ "@tealbrick/vision": "0.1.0",
39
+ "@tealbrick/deliver": "0.1.0",
40
+ "@inquirer/prompts": "8.7.2",
41
+ "zod": "^4.0.0"
42
+ },
43
+ "peerDependencies": {
44
+ "eve": ">=0.55.0 <0.56.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "eve": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "devDependencies": {
52
+ "typescript": "^5.9.3",
53
+ "@types/node": "^24.0.0"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "registry": "https://registry.npmjs.org"
58
+ },
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/Doppelabs/tealbrick-packages.git",
62
+ "directory": "packages/kit"
63
+ }
64
+ }