@tealbrick/kit 0.3.0-rc.1 → 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 (43) hide show
  1. package/BOOTSTRAP.md +9 -10
  2. package/BUZZ.md +146 -0
  3. package/NATIVE.md +10 -4
  4. package/README.md +263 -2
  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 +9 -4
  13. package/dist/index.d.ts +15 -14
  14. package/dist/index.js +1 -1
  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.js +7 -2
  22. package/dist/native-claude.d.ts +99 -0
  23. package/dist/native-claude.js +584 -0
  24. package/dist/native-enroll.d.ts +22 -0
  25. package/dist/native-enroll.js +85 -0
  26. package/dist/native-runtime-sync.d.ts +49 -0
  27. package/dist/native-runtime-sync.js +101 -0
  28. package/dist/native-selection.js +19 -4
  29. package/dist/native-serve-config.d.ts +286 -0
  30. package/dist/native-serve-config.js +296 -0
  31. package/dist/native-serve.d.ts +76 -0
  32. package/dist/native-serve.js +145 -0
  33. package/dist/native-setup.js +38 -15
  34. package/dist/native-standing-grants.d.ts +105 -0
  35. package/dist/native-standing-grants.js +226 -0
  36. package/dist/native.d.ts +3 -0
  37. package/dist/native.js +94 -5
  38. package/dist/onboarding.d.ts +8 -0
  39. package/dist/onboarding.js +9 -2
  40. package/dist/runtime-onboarding.js +2 -2
  41. package/dist/secrets.d.ts +2 -1
  42. package/dist/secrets.js +4 -2
  43. package/package.json +9 -7
@@ -0,0 +1,105 @@
1
+ import type { ClaudeProfile, StandingGrant } from './native-serve-config.js';
2
+ /**
3
+ * Standing grants: owner-defined pre-approvals for bounded classes of approval-gated Teal Brick calls.
4
+ * The kit's PreToolUse hook (never the model) decides: a matching, unexpired grant with remaining caps
5
+ * replaces the per-payload prompt. Anything else falls back to the normal payload approval.
6
+ *
7
+ * Usage counters live in `<state>/standing-grants.json` (0600): only timestamps per grant id, pruned to
8
+ * the 24h window. `native serve` and `native acp` on the same kit root share the file. Every
9
+ * check-and-record runs under an exclusive lock file created with O_EXCL (`<file>.lock`), retried until
10
+ * a timeout; a lock older than staleLockMs is treated as abandoned (crashed holder) and removed. Writes are
11
+ * tmp + rename, so readers never see a partial file. If the lock cannot be taken or the file is unreadable,
12
+ * no standing approval is given (fail closed to the prompt).
13
+ */
14
+ export type GrantRefusal = 'per_day' | 'per_hour' | 'min_interval' | 'store_unavailable';
15
+ export interface GrantUsageStore {
16
+ now(): number;
17
+ /** Atomically checks the grant's caps and records one use when they allow it. */
18
+ consume(grant: StandingGrant): Promise<{
19
+ ok: true;
20
+ } | {
21
+ ok: false;
22
+ reason: GrantRefusal;
23
+ }>;
24
+ /** Read-only usage snapshot (timestamps per grant id), or undefined when unreadable. */
25
+ usage(): Promise<Record<string, number[]> | undefined>;
26
+ }
27
+ export declare function capRefusal(grant: StandingGrant, stamps: number[], now: number): GrantRefusal | undefined;
28
+ export interface GrantStoreOptions {
29
+ now?: () => number;
30
+ lockTimeoutMs?: number;
31
+ staleLockMs?: number;
32
+ }
33
+ /** Persistent store at `file`, or an in-memory store when `file` is undefined (harness-only tests). */
34
+ export declare function standingGrantStore(path: string | undefined, options?: GrantStoreOptions): GrantUsageStore;
35
+ export declare const grantExpired: (grant: StandingGrant, now: number) => boolean;
36
+ export type GrantRef = {
37
+ appId: string;
38
+ plugin?: string;
39
+ action?: string;
40
+ };
41
+ export interface GrantCall {
42
+ name: string;
43
+ input: Record<string, unknown>;
44
+ agentId?: string;
45
+ agentType?: string;
46
+ }
47
+ export type GrantDecision = {
48
+ grant: StandingGrant;
49
+ ref?: GrantRef;
50
+ };
51
+ export interface GrantContext {
52
+ profile: ClaudeProfile;
53
+ store: GrantUsageStore;
54
+ resolveApp?: (registrationId: string) => Promise<GrantRef | undefined>;
55
+ trace: (record: Record<string, unknown>) => void;
56
+ }
57
+ /**
58
+ * Finds the first configured grant (in order) that covers this approval-gated call and still has capacity,
59
+ * and records its use. Returns undefined when the normal payload approval must run.
60
+ */
61
+ export declare function standingGrantFor(ctx: GrantContext, call: GrantCall): Promise<GrantDecision | undefined>;
62
+ /** Owner-facing audit view: no deny patterns or inputs, only identifiers, caps, expiry and remaining uses. */
63
+ export declare function standingGrantSummary(grants: StandingGrant[], store: GrantUsageStore): Promise<({
64
+ denyPatterns: number;
65
+ remainingThisHour?: number | undefined;
66
+ usedToday: number;
67
+ remainingToday: number;
68
+ id: string;
69
+ description: string;
70
+ operation: string;
71
+ match: {
72
+ registrationId?: string | undefined;
73
+ plugin?: string | string[] | undefined;
74
+ action?: string | string[] | undefined;
75
+ };
76
+ experts: string[];
77
+ caps: {
78
+ perDay: number;
79
+ perHour?: number | undefined;
80
+ minIntervalSeconds?: number | undefined;
81
+ };
82
+ expires: string;
83
+ expired: boolean;
84
+ } | {
85
+ denyPatterns: number;
86
+ usage: string;
87
+ id: string;
88
+ description: string;
89
+ operation: string;
90
+ match: {
91
+ registrationId?: string | undefined;
92
+ plugin?: string | string[] | undefined;
93
+ action?: string | string[] | undefined;
94
+ };
95
+ experts: string[];
96
+ caps: {
97
+ perDay: number;
98
+ perHour?: number | undefined;
99
+ minIntervalSeconds?: number | undefined;
100
+ };
101
+ expires: string;
102
+ expired: boolean;
103
+ })[]>;
104
+ /** System-prompt note for the owner session; empty when no unexpired grant exists. */
105
+ export declare function standingGrantNote(grants: StandingGrant[], now: number): string;
@@ -0,0 +1,226 @@
1
+ import { lstat, mkdir, open, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { dirname } from 'node:path';
4
+ const HOUR = 3_600_000, DAY = 24 * HOUR;
5
+ export function capRefusal(grant, stamps, now) {
6
+ const day = stamps.filter(t => t > now - DAY && t <= now);
7
+ if (day.length >= grant.caps.perDay)
8
+ return 'per_day';
9
+ if (grant.caps.perHour !== undefined && day.filter(t => t > now - HOUR).length >= grant.caps.perHour)
10
+ return 'per_hour';
11
+ if (grant.caps.minIntervalSeconds !== undefined && day.some(t => t > now - grant.caps.minIntervalSeconds * 1000))
12
+ return 'min_interval';
13
+ return undefined;
14
+ }
15
+ const prune = (usage, now) => {
16
+ const out = {};
17
+ for (const [id, stamps] of Object.entries(usage)) {
18
+ const kept = stamps.filter(t => t > now - DAY).sort((a, b) => a - b).slice(-200);
19
+ if (kept.length)
20
+ out[id] = kept;
21
+ }
22
+ return out;
23
+ };
24
+ function parseUsage(text) {
25
+ const value = JSON.parse(text);
26
+ if (!value || value.schema !== 1 || typeof value.usage !== 'object' || value.usage === null || Array.isArray(value.usage))
27
+ throw Error('kit_standing_grants_store_invalid');
28
+ const out = {};
29
+ for (const [id, stamps] of Object.entries(value.usage)) {
30
+ if (!/^[a-z0-9][a-z0-9-]{1,47}$/.test(id) || !Array.isArray(stamps) || !stamps.every(t => Number.isFinite(t)))
31
+ throw Error('kit_standing_grants_store_invalid');
32
+ out[id] = stamps;
33
+ }
34
+ return out;
35
+ }
36
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
37
+ /** Persistent store at `file`, or an in-memory store when `file` is undefined (harness-only tests). */
38
+ export function standingGrantStore(path, options = {}) {
39
+ const now = options.now ?? Date.now, lockTimeout = options.lockTimeoutMs ?? 3000, stale = options.staleLockMs ?? 30_000;
40
+ if (!path) {
41
+ const memory = {};
42
+ return { now, async usage() { return prune(memory, now()); },
43
+ async consume(grant) { const t = now(), stamps = memory[grant.id] ?? [], why = capRefusal(grant, stamps, t); if (why)
44
+ return { ok: false, reason: why }; memory[grant.id] = [...stamps.filter(s => s > t - DAY), t]; return { ok: true }; } };
45
+ }
46
+ const file = path, lock = file + '.lock';
47
+ // In-process serialisation keeps same-process callers from contending on the lock file.
48
+ let chain = Promise.resolve();
49
+ const read = async () => {
50
+ try {
51
+ const s = await lstat(file);
52
+ if (!s.isFile() || s.isSymbolicLink())
53
+ throw Error('kit_standing_grants_store_unsafe');
54
+ }
55
+ catch (e) {
56
+ if (e.code === 'ENOENT')
57
+ return {};
58
+ throw e;
59
+ }
60
+ return parseUsage(await readFile(file, 'utf8'));
61
+ };
62
+ async function acquire() {
63
+ const deadline = Date.now() + lockTimeout;
64
+ await mkdir(dirname(file), { recursive: true, mode: 0o700 });
65
+ for (;;) {
66
+ try {
67
+ const h = await open(lock, 'wx', 0o600);
68
+ await h.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));
69
+ await h.close();
70
+ return true;
71
+ }
72
+ catch (e) {
73
+ if (e.code !== 'EEXIST')
74
+ return false;
75
+ }
76
+ try {
77
+ if (Date.now() - (await stat(lock)).mtimeMs > stale)
78
+ await unlink(lock).catch(() => { });
79
+ }
80
+ catch { }
81
+ if (Date.now() > deadline)
82
+ return false;
83
+ await sleep(15 + Math.random() * 20);
84
+ }
85
+ }
86
+ async function locked(fn, busy) {
87
+ const run = chain.then(async () => {
88
+ try {
89
+ if (!await acquire())
90
+ return busy;
91
+ }
92
+ catch {
93
+ return busy;
94
+ }
95
+ try {
96
+ return await fn();
97
+ }
98
+ catch {
99
+ return busy;
100
+ }
101
+ finally {
102
+ await unlink(lock).catch(() => { });
103
+ }
104
+ });
105
+ chain = run;
106
+ return run;
107
+ }
108
+ return {
109
+ now,
110
+ async usage() { try {
111
+ return prune(await read(), now());
112
+ }
113
+ catch {
114
+ return undefined;
115
+ } },
116
+ consume(grant) {
117
+ return locked(async () => {
118
+ let usage;
119
+ try {
120
+ usage = await read();
121
+ }
122
+ catch {
123
+ return { ok: false, reason: 'store_unavailable' };
124
+ }
125
+ const t = now();
126
+ usage = prune(usage, t);
127
+ const why = capRefusal(grant, usage[grant.id] ?? [], t);
128
+ if (why)
129
+ return { ok: false, reason: why };
130
+ usage[grant.id] = [...(usage[grant.id] ?? []), t];
131
+ const tmp = file + '.' + randomUUID();
132
+ try {
133
+ await writeFile(tmp, JSON.stringify({ schema: 1, usage }) + '\n', { flag: 'wx', mode: 0o600 });
134
+ await rename(tmp, file);
135
+ }
136
+ catch {
137
+ await unlink(tmp).catch(() => { });
138
+ return { ok: false, reason: 'store_unavailable' };
139
+ }
140
+ return { ok: true };
141
+ }, { ok: false, reason: 'store_unavailable' });
142
+ },
143
+ };
144
+ }
145
+ const norm = (op) => op.replaceAll('.', '_');
146
+ const globMatch = (pattern, value, ci) => {
147
+ const re = new RegExp('^' + pattern.split('*').map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$', ci ? 'is' : 's');
148
+ return re.test(value);
149
+ };
150
+ const anyGlob = (patterns, value, ci) => value !== undefined && (Array.isArray(patterns) ? patterns : [patterns]).some(p => globMatch(p, value, ci));
151
+ export const grantExpired = (grant, now) => !(Date.parse(grant.expires) > now);
152
+ /**
153
+ * Finds the first configured grant (in order) that covers this approval-gated call and still has capacity,
154
+ * and records its use. Returns undefined when the normal payload approval must run.
155
+ */
156
+ export async function standingGrantFor(ctx, call) {
157
+ const { profile, store } = ctx, { name, input } = call;
158
+ if (!profile.standingGrants.length || name !== 'mcp__tealbrick__tealbrick_call' || typeof input.operation !== 'string' || typeof input.registrationId !== 'string')
159
+ return undefined;
160
+ const op = norm(input.operation), registrationId = input.registrationId, child = call.agentId !== undefined;
161
+ let ref, resolved = false;
162
+ const resolve = async () => { if (!resolved) {
163
+ resolved = true;
164
+ try {
165
+ ref = await ctx.resolveApp?.(registrationId);
166
+ }
167
+ catch {
168
+ ref = undefined;
169
+ }
170
+ } return ref; };
171
+ const body = JSON.stringify(input.input ?? null).toLowerCase();
172
+ for (const grant of profile.standingGrants) {
173
+ if (norm(grant.operation) !== op)
174
+ continue;
175
+ if (child && !(call.agentType !== undefined && grant.experts?.includes(call.agentType) && Object.hasOwn(profile.contracts, call.agentType)))
176
+ continue;
177
+ const m = grant.match;
178
+ if (m.registrationId !== undefined && !globMatch(m.registrationId, registrationId, false))
179
+ continue;
180
+ if (m.plugin !== undefined || m.action !== undefined) {
181
+ const r = await resolve();
182
+ if (!r || r.plugin === undefined || r.action === undefined)
183
+ continue;
184
+ if (m.plugin !== undefined && !anyGlob(m.plugin, r.plugin, true))
185
+ continue;
186
+ if (m.action !== undefined && !anyGlob(m.action, r.action, true))
187
+ continue;
188
+ }
189
+ if (grant.deny?.inputPattern?.some(p => body.includes(p.toLowerCase()))) {
190
+ ctx.trace({ event: 'standing_grant.skipped', grantId: grant.id, reason: 'deny_pattern', operation: input.operation, registrationId });
191
+ continue;
192
+ }
193
+ if (grantExpired(grant, store.now())) {
194
+ ctx.trace({ event: 'standing_grant.exhausted', grantId: grant.id, reason: 'expired', operation: input.operation, registrationId });
195
+ continue;
196
+ }
197
+ const used = await store.consume(grant);
198
+ if (!used.ok) {
199
+ ctx.trace({ event: 'standing_grant.exhausted', grantId: grant.id, reason: used.reason, operation: input.operation, registrationId });
200
+ continue;
201
+ }
202
+ return { grant, ...(ref ? { ref } : {}) };
203
+ }
204
+ return undefined;
205
+ }
206
+ /** Owner-facing audit view: no deny patterns or inputs, only identifiers, caps, expiry and remaining uses. */
207
+ export async function standingGrantSummary(grants, store) {
208
+ const usage = await store.usage(), now = store.now();
209
+ return grants.map(g => {
210
+ const stamps = (usage?.[g.id] ?? []).filter(t => t > now - DAY && t <= now), hour = stamps.filter(t => t > now - HOUR).length;
211
+ return { id: g.id, description: g.description, operation: g.operation, match: g.match, experts: [...(g.experts ?? [])], caps: g.caps, expires: g.expires,
212
+ expired: grantExpired(g, now), ...(usage ? { usedToday: stamps.length, remainingToday: Math.max(0, g.caps.perDay - stamps.length),
213
+ ...(g.caps.perHour !== undefined ? { remainingThisHour: Math.max(0, g.caps.perHour - hour) } : {}) } : { usage: 'unavailable' }),
214
+ denyPatterns: g.deny?.inputPattern?.length ?? 0 };
215
+ });
216
+ }
217
+ /** System-prompt note for the owner session; empty when no unexpired grant exists. */
218
+ export function standingGrantNote(grants, now) {
219
+ const live = grants.filter(g => !grantExpired(g, now));
220
+ if (!live.length)
221
+ return '';
222
+ const caps = (g) => [`${g.caps.perDay}/day`, ...(g.caps.perHour !== undefined ? [`${g.caps.perHour}/hour`] : []), ...(g.caps.minIntervalSeconds !== undefined ? [`at least ${g.caps.minIntervalSeconds}s apart`] : [])].join(', ');
223
+ return ['Standing grants: your owner pre-approved these classes of calls; the harness applies them automatically (you do not choose them) within their caps until they expire:',
224
+ ...live.map(g => `- ${g.id} (${g.operation}): ${g.description} [${caps(g)}; expires ${g.expires}]`),
225
+ 'Anything outside these grants, or beyond their caps, still needs the owner\'s approval of the exact payload. Spending money, contracts or signatures, and first contact with strangers are never covered unless a grant explicitly says so.'].join('\n');
226
+ }
package/dist/native.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { guidedNativeSetup, nativeSdkConfig } from './native-setup.js';
2
2
  import { type PortalLogin } from './onboarding.js';
3
3
  import { type KnowledgeOperation } from '@tealbrick/portal/runtime';
4
+ import { MarketplaceRuntimeConnector } from '@tealbrick/portal/marketplace-runtime';
4
5
  declare const harnesses: {
5
6
  readonly claude: "Claude Agent SDK";
6
7
  readonly codex: "Codex";
@@ -22,6 +23,7 @@ export declare function setupNative(root: string, input: {
22
23
  /** A fresh connector per invocation rechecks Portal and local activation. No authorization cache
23
24
  * survives MCP reconnect, local deactivation or a request boundary. No session is created here. */
24
25
  export declare function nativeConnector(): {
26
+ marketplace: () => Promise<MarketplaceRuntimeConnector | undefined>;
25
27
  capabilities: () => Promise<{
26
28
  revision: string;
27
29
  expiresAt: number;
@@ -35,5 +37,6 @@ export declare function nativeConnector(): {
35
37
  }>;
36
38
  call: (registrationId: string, operation: KnowledgeOperation, input?: Record<string, unknown>, signal?: AbortSignal) => Promise<any>;
37
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 acp [--root DIRECTORY] [--buzz-cli PATH] | native enable|disable | native marketplace-enable|marketplace-disable";
38
41
  export declare function nativeCommand(args: string[], root: string): Promise<void>;
39
42
  export { guidedNativeSetup, nativeSdkConfig };
package/dist/native.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { guidedNativeSetup, nativeSdkConfig } from './native-setup.js';
3
3
  import { applyKit, readKit } from './index.js';
4
- import { loginPortal, portalOrigin } from './onboarding.js';
4
+ import { deviceLabel, loginPortal, portalOrigin } from './onboarding.js';
5
5
  import { PortalHttpError } from '@tealbrick/portal';
6
6
  import { RuntimeConnector } from '@tealbrick/portal/runtime';
7
+ import { MarketplaceRuntimeConnector } from '@tealbrick/portal/marketplace-runtime';
7
8
  import { serveMcp } from '@tealbrick/portal/mcp';
8
9
  import { runtimeSecret } from './secrets.js';
9
10
  const harnesses = { claude: 'Claude Agent SDK', codex: 'Codex', langchain: 'LangChain' };
@@ -30,7 +31,7 @@ export async function setupNative(root, input, login) {
30
31
  const enrolled = await login.client.request('/api/runtime/connections', { workspaceId: input.workspaceId, nodeId: input.nodeId, agentId: input.agentId });
31
32
  if (!enrolled.connectionId || !enrolled.credential || !Number.isFinite(enrolled.expiresAt) || enrolled.expiresAt <= Date.now())
32
33
  throw Error('kit_runtime_enrollment_failed');
33
- await applyKit(root, { version: 1, native: { harness: input.harness, active: true }, runtime: { issuer: login.issuer, org: login.org, workspaceId: input.workspaceId, agentId: input.agentId, connectionId: enrolled.connectionId, credentialRef: 'TEALBRICK_RUNTIME_CREDENTIAL', bindings: old.runtime?.bindings ?? {} } }, { TEALBRICK_RUNTIME_CREDENTIAL: enrolled.credential });
34
+ await applyKit(root, { version: 1, native: { harness: input.harness, active: true, ...(old.native?.marketplace === undefined ? {} : { marketplace: old.native.marketplace }) }, runtime: { issuer: login.issuer, org: login.org, workspaceId: input.workspaceId, agentId: input.agentId, connectionId: enrolled.connectionId, credentialRef: 'TEALBRICK_RUNTIME_CREDENTIAL', bindings: old.runtime?.bindings ?? {} } }, { TEALBRICK_RUNTIME_CREDENTIAL: enrolled.credential });
34
35
  return { connected: true, agentId: input.agentId, connectionId: enrolled.connectionId, harness: input.harness };
35
36
  }
36
37
  /** A fresh connector per invocation rechecks Portal and local activation. No authorization cache
@@ -43,10 +44,89 @@ export function nativeConnector() {
43
44
  const { credentialRef, ...runtime } = config.runtime;
44
45
  return new RuntimeConnector({ ...runtime, credential: () => runtimeSecret(credentialRef), resolveCredential: runtimeSecret });
45
46
  }
46
- return { capabilities: async () => (await current()).capabilities(true), call: async (registrationId, operation, input = {}, signal) => (await current()).call(registrationId, operation, input, signal) };
47
+ return { marketplace: async () => { const config = await readKit(process.cwd()); if (!config.native?.active || !config.native.marketplace || !config.runtime)
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) };
47
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
+ }
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';
48
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
+ }
79
+ if (args[0] === 'serve' || args[0] === 'enroll') {
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']);
81
+ if (typeof flags.root === 'string') {
82
+ root = resolve(flags.root);
83
+ process.chdir(root);
84
+ }
85
+ if (args[0] === 'serve') {
86
+ const { serveNative, installLaunchd, uninstallLaunchd } = await import('./native-serve.js');
87
+ if (flags['install-launchd'] && flags.uninstall)
88
+ throw Error('kit_native_invalid_arguments');
89
+ if (flags.uninstall) {
90
+ console.log(JSON.stringify(await uninstallLaunchd(root)));
91
+ return;
92
+ }
93
+ if (flags['install-launchd']) {
94
+ console.log(JSON.stringify(await installLaunchd(root, { load: !flags['no-load'] })));
95
+ return;
96
+ }
97
+ if (flags['no-load'])
98
+ throw Error('kit_native_invalid_arguments');
99
+ await serveNative(root);
100
+ return;
101
+ }
102
+ if (typeof flags.url !== 'string')
103
+ throw Error('kit_native_public_url_required');
104
+ const { enrollNative } = await import('./native-enroll.js');
105
+ const state = await readKit(root);
106
+ if (!state.native || !state.runtime)
107
+ throw Error('kit_native_setup_required');
108
+ const login = await loginPortal(portalOrigin(state.runtime.issuer), (url, code) => console.error(`Approve Tealbrick endpoint registration: ${url}\nCode: ${code}`), { label: deviceLabel('native enroll', root) });
109
+ try {
110
+ const result = await enrollNative(root, { url: flags.url, ...(typeof flags.port === 'string' ? { port: Number(flags.port) } : {}) }, login, { verifyEndpoint: !flags['skip-endpoint-check'] });
111
+ console.log(JSON.stringify(result));
112
+ if (!flags['skip-endpoint-check'] && !result.endpointVerified) {
113
+ console.error('Portal registration is saved. Start `tealbrick native serve` and the HTTPS proxy, then re-run enroll to verify the endpoint.');
114
+ process.exitCode = 2;
115
+ }
116
+ }
117
+ catch (error) {
118
+ if (error instanceof PortalHttpError && /^[a-z_]+$/.test(error.code ?? ''))
119
+ throw Error('kit_portal_' + error.code);
120
+ throw error;
121
+ }
122
+ return;
123
+ }
49
124
  const command = args[0], flags = {};
125
+ // A bare `tealbrick native` or help request prints usage; it is not a failed setup.
126
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
127
+ console.log(nativeUsage);
128
+ return;
129
+ }
50
130
  for (let i = 1; i < args.length; i += 2) {
51
131
  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)])
52
132
  throw Error('kit_native_invalid_arguments');
@@ -68,7 +148,7 @@ export async function nativeCommand(args, root) {
68
148
  flags.delegation = 'disabled';
69
149
  }
70
150
  const issuer = portalOrigin(flags.issuer ?? 'https://portal.tealbrick.com');
71
- const login = await loginPortal(issuer, (url, code) => console.error(`Approve Tealbrick setup: ${url}\nCode: ${code}`));
151
+ const login = await loginPortal(issuer, (url, code) => console.error(`Approve Tealbrick setup: ${url}\nCode: ${code}`), { label: deviceLabel(`native setup (${flags.harness})`, root) });
72
152
  const { select, input } = await import('@inquirer/prompts');
73
153
  try {
74
154
  console.log(JSON.stringify(await guidedNativeSetup(root, flags, login, { choose: (message, choices) => select({ message, choices }), name: message => input({ message }) })));
@@ -91,6 +171,14 @@ export async function nativeCommand(args, root) {
91
171
  await serveMcp(nativeConnector());
92
172
  return;
93
173
  }
174
+ if (command === 'marketplace-enable' || command === 'marketplace-disable') {
175
+ const old = await readKit(root);
176
+ if (!old.native)
177
+ throw Error('kit_native_setup_required');
178
+ await applyKit(root, { ...old, native: { ...old.native, marketplace: command === 'marketplace-enable' } });
179
+ console.log(JSON.stringify({ marketplace: command === 'marketplace-enable' }));
180
+ return;
181
+ }
94
182
  if (command === 'disable' || command === 'enable') {
95
183
  const old = await readKit(root);
96
184
  if (!old.native)
@@ -99,6 +187,7 @@ export async function nativeCommand(args, root) {
99
187
  console.log(JSON.stringify({ active: command === 'enable' }));
100
188
  return;
101
189
  }
102
- 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');
190
+ console.error(nativeUsage);
191
+ process.exitCode = 2;
103
192
  }
104
193
  export { guidedNativeSetup, nativeSdkConfig };
@@ -8,15 +8,23 @@ 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;
14
19
  export declare function agentOrigin(value: string): string;
15
20
  /** Device authorization is interactive; user/refresh tokens never enter kit state. */
21
+ /** Label shown in Portal → Connected access: purpose, host and kit folder, so owners can tell devices apart (portal-core#15). */
22
+ export declare function deviceLabel(purpose: string, root?: string, host?: string): string;
16
23
  export declare function loginPortal(issuer: string, show: (url: string, code: string) => void, options?: {
17
24
  fetch?: typeof fetch;
18
25
  sleep?: (ms: number) => Promise<void>;
19
26
  now?: () => number;
27
+ label?: string;
20
28
  }): Promise<PortalLogin>;
21
29
  /** Reuse Portal's owned registration + revision-checked canvas APIs, not a parallel card store. */
22
30
  export declare function registerPortalCard(login: PortalLogin, input: {
@@ -1,4 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { hostname } from 'node:os';
3
+ import { basename } from 'node:path';
2
4
  import { PortalClient, PortalHttpError } from '@tealbrick/portal';
3
5
  /** Reject URLs that would leak credentials or be silently truncated by registration. */
4
6
  export function portalOrigin(value) {
@@ -14,10 +16,15 @@ export function agentOrigin(value) {
14
16
  return url.origin;
15
17
  }
16
18
  /** Device authorization is interactive; user/refresh tokens never enter kit state. */
19
+ /** Label shown in Portal → Connected access: purpose, host and kit folder, so owners can tell devices apart (portal-core#15). */
20
+ export function deviceLabel(purpose, root = process.cwd(), host = hostname()) {
21
+ const folder = basename(root) || root;
22
+ return `Teal Brick ${purpose} · ${host.replace(/\.local$/, '')} · ${folder}`.replace(/[\u0000-\u001f]/g, '').slice(0, 120);
23
+ }
17
24
  export async function loginPortal(issuer, show, options = {}) {
18
25
  issuer = portalOrigin(issuer);
19
26
  const client = new PortalClient({ issuer, fetch: options.fetch });
20
- const device = await client.startDevice({ deviceId: randomUUID(), deviceLabel: 'Teal Brick package setup' });
27
+ const device = await client.startDevice({ deviceId: randomUUID(), deviceLabel: options.label ?? deviceLabel('setup') });
21
28
  const verification = new URL(device.verification_uri_complete);
22
29
  if (verification.origin !== issuer || verification.username || verification.password)
23
30
  throw Error('kit_portal_verification_origin_mismatch');
@@ -53,7 +60,7 @@ export async function loginPortal(issuer, show, options = {}) {
53
60
  const directory = await authenticated.request('/api/me/workspaces');
54
61
  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
62
  throw Error('kit_portal_account_mismatch');
56
- return { issuer, client: authenticated, org: account.user.orgId, userId: account.user.id, workspaces: directory.workspaces };
63
+ return { issuer, client: authenticated, org: account.user.orgId, userId: account.user.id, workspaces: directory.workspaces, identity: { token: session.token, bundle: session.bundle } };
57
64
  }
58
65
  /** Reuse Portal's owned registration + revision-checked canvas APIs, not a parallel card store. */
59
66
  export async function registerPortalCard(login, input) {
@@ -1,6 +1,6 @@
1
1
  import { password } from '@inquirer/prompts';
2
2
  import { createHash } from 'node:crypto';
3
- import { loginPortal, portalOrigin } from './onboarding.js';
3
+ import { deviceLabel, loginPortal, portalOrigin } from './onboarding.js';
4
4
  import { readKit, applyKit } from './index.js';
5
5
  function flags(args) { const out = {}; for (let i = 0; i < args.length; i += 2) {
6
6
  if (!/^--[a-z]+$/.test(args[i]) || !args[i + 1] || args[i + 1].startsWith('--'))
@@ -37,7 +37,7 @@ finally {
37
37
  export async function runtimeOnboarding(command, args, root) {
38
38
  const f = flags(args), old = await readKit(root), issuer = portalOrigin(f.issuer ?? old.portal?.issuer ?? 'https://portal.tealbrick.com');
39
39
  const workspaceId = required(f, 'workspace'), nodeId = required(f, 'node');
40
- const login = await loginPortal(issuer, (url, code) => console.log(`Approve this customer runtime in Portal: ${url}\nCode: ${code}`));
40
+ const login = await loginPortal(issuer, (url, code) => console.log(`Approve this customer runtime in Portal: ${url}\nCode: ${code}`), { label: deviceLabel(`runtime ${command}`, root) });
41
41
  if (!login.workspaces.some(w => w.id === workspaceId))
42
42
  throw Error('kit_portal_workspace_required');
43
43
  if (command === 'connect') {
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.1",
3
+ "version": "0.3.0-rc.10",
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.1",
47
- "@tealbrick/avm": "0.3.0-rc.1",
48
- "@tealbrick/voice": "0.3.0-rc.1",
49
- "@tealbrick/vision": "0.3.0-rc.1",
50
- "@tealbrick/deliver": "0.3.0-rc.1",
48
+ "@tealbrick/portal": "0.3.0-rc.10",
49
+ "@tealbrick/avm": "0.3.0-rc.10",
50
+ "@tealbrick/voice": "0.3.0-rc.10",
51
+ "@tealbrick/vision": "0.3.0-rc.10",
52
+ "@tealbrick/deliver": "0.3.0-rc.10",
51
53
  "@inquirer/prompts": "8.7.2",
52
54
  "zod": "^4.0.0"
53
55
  },