@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.
- package/BOOTSTRAP.md +3 -3
- package/NATIVE.md +3 -3
- package/README.md +123 -2
- package/dist/cli.js +1 -1
- package/dist/native-claude.d.ts +49 -0
- package/dist/native-claude.js +287 -0
- package/dist/native-enroll.d.ts +22 -0
- package/dist/native-enroll.js +85 -0
- package/dist/native-runtime-sync.d.ts +49 -0
- package/dist/native-runtime-sync.js +101 -0
- package/dist/native-serve-config.d.ts +160 -0
- package/dist/native-serve-config.js +190 -0
- package/dist/native-serve.d.ts +69 -0
- package/dist/native-serve.js +128 -0
- package/dist/native.js +63 -1
- package/dist/onboarding.d.ts +5 -0
- package/dist/onboarding.js +1 -1
- package/dist/secrets.d.ts +2 -1
- package/dist/secrets.js +4 -2
- package/package.json +6 -6
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { RuntimeConnector } from '@tealbrick/portal/runtime';
|
|
2
|
+
import type { JWTVerifyGetKey } from 'jose';
|
|
3
|
+
export type RuntimeCapabilities = Awaited<ReturnType<RuntimeConnector['capabilities']>>;
|
|
4
|
+
export type RuntimeSyncStatus = 'starting' | 'synced' | 'retrying' | 'inactive' | 'stopped';
|
|
5
|
+
export interface RuntimeSyncSnapshot {
|
|
6
|
+
status: RuntimeSyncStatus;
|
|
7
|
+
revision?: string;
|
|
8
|
+
expiresAt?: number;
|
|
9
|
+
lastSyncAt?: number;
|
|
10
|
+
failures: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
capabilities?: RuntimeCapabilities;
|
|
13
|
+
}
|
|
14
|
+
export interface RuntimeSyncOptions {
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
keys?: JWTVerifyGetKey;
|
|
17
|
+
log?: (event: Record<string, unknown>) => void;
|
|
18
|
+
/** Base delay between successful syncs. Portal's signed config lives 300s, so this must stay well below it. */
|
|
19
|
+
intervalMs?: number;
|
|
20
|
+
jitterMs?: number;
|
|
21
|
+
/** First retry delay after a failure; doubles per consecutive failure up to maxRetryMs. */
|
|
22
|
+
retryMs?: number;
|
|
23
|
+
maxRetryMs?: number;
|
|
24
|
+
random?: () => number;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Keeps this customer runtime's Portal configuration acknowledged while `native serve` is idle.
|
|
29
|
+
* The per-turn Teal Brick MCP child still fetches a fresh authorization for every call; this loop
|
|
30
|
+
* only fetches the signed config, acknowledges it (Portal derives runtime sync freshness from that)
|
|
31
|
+
* and holds the latest capability snapshot in memory. App credentials never leave the kit root.
|
|
32
|
+
*/
|
|
33
|
+
export declare function startRuntimeSync(root: string, options?: RuntimeSyncOptions): {
|
|
34
|
+
snapshot: () => RuntimeSyncSnapshot;
|
|
35
|
+
capabilities: () => {
|
|
36
|
+
revision: string;
|
|
37
|
+
expiresAt: number;
|
|
38
|
+
apps: {
|
|
39
|
+
registrationId: string;
|
|
40
|
+
appId: "knowledge";
|
|
41
|
+
actions: import("@tealbrick/portal/runtime").CrudAction[];
|
|
42
|
+
available: boolean;
|
|
43
|
+
operations: import("@tealbrick/portal/runtime").KnowledgeOperation[];
|
|
44
|
+
}[];
|
|
45
|
+
} | undefined;
|
|
46
|
+
syncNow: () => Promise<void>;
|
|
47
|
+
stop(): Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
export type RuntimeSync = ReturnType<typeof startRuntimeSync>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { RuntimeConnector } from '@tealbrick/portal/runtime';
|
|
2
|
+
import { readKit } from './index.js';
|
|
3
|
+
import { runtimeSecret } from './secrets.js';
|
|
4
|
+
const defaults = { intervalMs: 90_000, jitterMs: 30_000, retryMs: 15_000, maxRetryMs: 240_000 };
|
|
5
|
+
const code = (v) => typeof v === 'string' && /^[a-z][a-z0-9_]{0,80}$/.test(v) ? v : undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Keeps this customer runtime's Portal configuration acknowledged while `native serve` is idle.
|
|
8
|
+
* The per-turn Teal Brick MCP child still fetches a fresh authorization for every call; this loop
|
|
9
|
+
* only fetches the signed config, acknowledges it (Portal derives runtime sync freshness from that)
|
|
10
|
+
* and holds the latest capability snapshot in memory. App credentials never leave the kit root.
|
|
11
|
+
*/
|
|
12
|
+
export function startRuntimeSync(root, options = {}) {
|
|
13
|
+
const o = { ...defaults, ...options }, random = o.random ?? Math.random, now = o.now ?? Date.now;
|
|
14
|
+
const log = o.log ?? ((event) => process.stderr.write(JSON.stringify(event) + '\n'));
|
|
15
|
+
const base = o.fetch ?? fetch;
|
|
16
|
+
let state = { status: 'starting', failures: 0 };
|
|
17
|
+
let timer, stopped = false, running;
|
|
18
|
+
let connector, connectorKey;
|
|
19
|
+
let observed = {};
|
|
20
|
+
const observe = async (input, init) => {
|
|
21
|
+
const response = await base(input, init);
|
|
22
|
+
const path = new URL(input instanceof Request ? input.url : String(input)).pathname;
|
|
23
|
+
if (path === '/api/runtime/config' || path === '/api/runtime/ack') {
|
|
24
|
+
observed[path === '/api/runtime/config' ? 'config' : 'ack'] = response.status;
|
|
25
|
+
if (!response.ok)
|
|
26
|
+
observed.code = code(await response.clone().json().then((b) => b?.error ?? b?.code, () => undefined)) ?? observed.code;
|
|
27
|
+
}
|
|
28
|
+
return response;
|
|
29
|
+
};
|
|
30
|
+
const jitter = (ms, spread) => Math.max(1, Math.round(ms + (random() * 2 - 1) * spread));
|
|
31
|
+
function schedule(ms) { if (stopped)
|
|
32
|
+
return; clearTimeout(timer); timer = setTimeout(() => { void syncNow(); }, ms); timer.unref?.(); }
|
|
33
|
+
async function cycle() {
|
|
34
|
+
let kit;
|
|
35
|
+
try {
|
|
36
|
+
kit = await readKit(root);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
kit = undefined;
|
|
40
|
+
}
|
|
41
|
+
if (!kit?.native?.active || !kit.runtime) {
|
|
42
|
+
if (state.status !== 'inactive')
|
|
43
|
+
log({ event: 'native.runtime.sync.inactive', message: 'Teal Brick runtime is not active in this kit root; Portal sync is paused.' });
|
|
44
|
+
connector = undefined;
|
|
45
|
+
connectorKey = undefined;
|
|
46
|
+
state = { status: 'inactive', failures: 0 };
|
|
47
|
+
schedule(jitter(o.intervalMs, o.jitterMs));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const { credentialRef, ...runtime } = kit.runtime, key = JSON.stringify(kit.runtime);
|
|
51
|
+
if (!connector || key !== connectorKey) {
|
|
52
|
+
connector = new RuntimeConnector({ ...runtime, credential: () => runtimeSecret(credentialRef, root), resolveCredential: name => runtimeSecret(name, root), fetch: observe, ...(o.keys ? { keys: o.keys } : {}) });
|
|
53
|
+
connectorKey = key;
|
|
54
|
+
}
|
|
55
|
+
observed = {};
|
|
56
|
+
let capabilities;
|
|
57
|
+
try {
|
|
58
|
+
capabilities = await connector.capabilities(true);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const reason = code(error?.message) ?? 'runtime_sync_failed';
|
|
62
|
+
if (observed.config === 401) {
|
|
63
|
+
const why = observed.code ?? 'runtime_credential_invalid';
|
|
64
|
+
stopped = true;
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
state = { ...state, status: 'stopped', error: why };
|
|
67
|
+
log({ event: 'native.runtime.sync.stopped', reason: 'credential_revoked', code: why, message: `Portal rejected this runtime's credential (${why}); background runtime sync has stopped and Portal will show the runtime as expired. Re-enroll with \`tealbrick native setup\`, then restart native serve.` });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const failures = state.failures + 1, delay = jitter(Math.min(o.maxRetryMs, o.retryMs * 2 ** (failures - 1)), Math.min(o.maxRetryMs, o.retryMs * 2 ** (failures - 1)) * 0.2);
|
|
71
|
+
state = { ...state, status: 'retrying', failures, error: observed.code ?? reason };
|
|
72
|
+
log({ event: 'native.runtime.sync.failed', code: state.error, ...(observed.config !== undefined ? { status: observed.config } : {}), failures, retryInMs: delay });
|
|
73
|
+
schedule(delay);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const changed = state.status !== 'synced' || state.revision !== capabilities.revision;
|
|
77
|
+
const ackFailed = observed.ack !== undefined && !(observed.ack >= 200 && observed.ack < 300);
|
|
78
|
+
state = { status: ackFailed ? 'retrying' : 'synced', revision: capabilities.revision, expiresAt: capabilities.expiresAt, lastSyncAt: now(), failures: ackFailed ? state.failures + 1 : 0, capabilities, ...(ackFailed ? { error: observed.code ?? 'runtime_ack_failed' } : {}) };
|
|
79
|
+
if (ackFailed)
|
|
80
|
+
log({ event: 'native.runtime.ack.failed', code: state.error, status: observed.ack, failures: state.failures });
|
|
81
|
+
else if (changed)
|
|
82
|
+
log({ event: 'native.runtime.synced', revision: capabilities.revision.slice(0, 12), apps: capabilities.apps.length, available: capabilities.apps.filter(a => a.available).length, expiresAt: new Date(capabilities.expiresAt).toISOString() });
|
|
83
|
+
// Never let the acknowledged lease lapse between syncs; a failed ack retries on the short schedule.
|
|
84
|
+
const next = ackFailed ? jitter(o.retryMs, o.retryMs * 0.2) : jitter(o.intervalMs, o.jitterMs);
|
|
85
|
+
schedule(Math.max(1, Math.min(next, capabilities.expiresAt - now() - 30_000)));
|
|
86
|
+
}
|
|
87
|
+
async function syncNow() {
|
|
88
|
+
if (stopped)
|
|
89
|
+
return;
|
|
90
|
+
running ??= cycle().catch(error => { log({ event: 'native.runtime.sync.failed', code: code(error?.message) ?? 'runtime_sync_failed' }); schedule(o.maxRetryMs); }).finally(() => { running = undefined; });
|
|
91
|
+
return running;
|
|
92
|
+
}
|
|
93
|
+
void syncNow();
|
|
94
|
+
return {
|
|
95
|
+
snapshot: () => ({ ...state }),
|
|
96
|
+
capabilities: () => state.capabilities,
|
|
97
|
+
syncNow,
|
|
98
|
+
async stop() { stopped = true; clearTimeout(timer); await running; if (state.status !== 'stopped')
|
|
99
|
+
state = { ...state, status: 'stopped' }; },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const hostPattern: RegExp;
|
|
3
|
+
declare const stdio: z.ZodObject<{
|
|
4
|
+
type: z.ZodOptional<z.ZodLiteral<"stdio">>;
|
|
5
|
+
command: z.ZodString;
|
|
6
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
8
|
+
}, z.core.$strict>;
|
|
9
|
+
export declare const tealbrickCallGrant: z.ZodObject<{
|
|
10
|
+
registrationId: z.ZodString;
|
|
11
|
+
operation: z.ZodString;
|
|
12
|
+
inputEquals: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>>;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
declare const effort: z.ZodEnum<{
|
|
15
|
+
low: "low";
|
|
16
|
+
high: "high";
|
|
17
|
+
medium: "medium";
|
|
18
|
+
xhigh: "xhigh";
|
|
19
|
+
max: "max";
|
|
20
|
+
}>;
|
|
21
|
+
/** Tools an agent may add to its default (Restricted) session access: research and expert delegation, never shell or writes. */
|
|
22
|
+
export declare const defaultAccessTools: readonly ["WebSearch", "WebFetch", "Agent", "TodoWrite"];
|
|
23
|
+
export declare const serveConfigSchema: z.ZodObject<{
|
|
24
|
+
version: z.ZodLiteral<1>;
|
|
25
|
+
port: z.ZodNumber;
|
|
26
|
+
publicUrl: z.ZodOptional<z.ZodString>;
|
|
27
|
+
allowedHosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
|
+
maxActiveTurns: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
agent: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
id: z.ZodString;
|
|
31
|
+
name: z.ZodString;
|
|
32
|
+
ownerId: z.ZodString;
|
|
33
|
+
}, z.core.$strict>>;
|
|
34
|
+
claude: z.ZodOptional<z.ZodObject<{
|
|
35
|
+
sdkModule: z.ZodOptional<z.ZodString>;
|
|
36
|
+
cwd: z.ZodString;
|
|
37
|
+
instructionsFile: z.ZodOptional<z.ZodString>;
|
|
38
|
+
recipe: z.ZodOptional<z.ZodObject<{
|
|
39
|
+
path: z.ZodString;
|
|
40
|
+
agent: z.ZodString;
|
|
41
|
+
}, z.core.$strict>>;
|
|
42
|
+
model: z.ZodOptional<z.ZodString>;
|
|
43
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
44
|
+
low: "low";
|
|
45
|
+
high: "high";
|
|
46
|
+
medium: "medium";
|
|
47
|
+
xhigh: "xhigh";
|
|
48
|
+
max: "max";
|
|
49
|
+
}>>;
|
|
50
|
+
maxTurns: z.ZodOptional<z.ZodNumber>;
|
|
51
|
+
maxBudgetUsd: z.ZodOptional<z.ZodNumber>;
|
|
52
|
+
timeoutSeconds: z.ZodOptional<z.ZodNumber>;
|
|
53
|
+
defaultTools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
54
|
+
WebSearch: "WebSearch";
|
|
55
|
+
WebFetch: "WebFetch";
|
|
56
|
+
Agent: "Agent";
|
|
57
|
+
TodoWrite: "TodoWrite";
|
|
58
|
+
}>>>;
|
|
59
|
+
experts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
60
|
+
promptFile: z.ZodString;
|
|
61
|
+
description: z.ZodOptional<z.ZodString>;
|
|
62
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
63
|
+
WebSearch: "WebSearch";
|
|
64
|
+
WebFetch: "WebFetch";
|
|
65
|
+
Read: "Read";
|
|
66
|
+
Glob: "Glob";
|
|
67
|
+
Grep: "Grep";
|
|
68
|
+
}>>>;
|
|
69
|
+
maxTurns: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
71
|
+
low: "low";
|
|
72
|
+
high: "high";
|
|
73
|
+
medium: "medium";
|
|
74
|
+
xhigh: "xhigh";
|
|
75
|
+
max: "max";
|
|
76
|
+
}>>;
|
|
77
|
+
}, z.core.$strict>>>;
|
|
78
|
+
nativeTools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>>;
|
|
79
|
+
readTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
80
|
+
mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
81
|
+
type: z.ZodOptional<z.ZodLiteral<"stdio">>;
|
|
82
|
+
command: z.ZodString;
|
|
83
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
84
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
85
|
+
}, z.core.$strict>>>;
|
|
86
|
+
tealbrickCalls: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
87
|
+
registrationId: z.ZodString;
|
|
88
|
+
operation: z.ZodString;
|
|
89
|
+
inputEquals: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>>;
|
|
90
|
+
}, z.core.$strict>>>;
|
|
91
|
+
requireSubscription: z.ZodOptional<z.ZodBoolean>;
|
|
92
|
+
envPassthrough: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
93
|
+
}, z.core.$strict>>;
|
|
94
|
+
codex: z.ZodOptional<z.ZodObject<{
|
|
95
|
+
codexBin: z.ZodString;
|
|
96
|
+
cwd: z.ZodString;
|
|
97
|
+
}, z.core.$strict>>;
|
|
98
|
+
}, z.core.$strict>;
|
|
99
|
+
export type ServeConfig = z.infer<typeof serveConfigSchema>;
|
|
100
|
+
export interface ClaudeProfile {
|
|
101
|
+
cwd: string;
|
|
102
|
+
sdkModule?: string;
|
|
103
|
+
instructions?: string;
|
|
104
|
+
model?: string;
|
|
105
|
+
effort?: z.infer<typeof effort>;
|
|
106
|
+
maxTurns: number;
|
|
107
|
+
maxBudgetUsd?: number;
|
|
108
|
+
timeoutSeconds: number;
|
|
109
|
+
defaultTools: string[];
|
|
110
|
+
experts: Record<string, ClaudeExpert>;
|
|
111
|
+
nativeTools: string[];
|
|
112
|
+
readTools: string[];
|
|
113
|
+
mcpServers: Record<string, z.infer<typeof stdio>>;
|
|
114
|
+
tealbrickCalls: Array<z.infer<typeof tealbrickCallGrant>>;
|
|
115
|
+
requireSubscription: boolean;
|
|
116
|
+
envPassthrough: string[];
|
|
117
|
+
}
|
|
118
|
+
export interface ClaudeExpert {
|
|
119
|
+
description: string;
|
|
120
|
+
prompt: string;
|
|
121
|
+
tools: string[];
|
|
122
|
+
maxTurns: number;
|
|
123
|
+
effort?: z.infer<typeof effort>;
|
|
124
|
+
}
|
|
125
|
+
export declare const servePaths: (root: string) => {
|
|
126
|
+
config: string;
|
|
127
|
+
state: string;
|
|
128
|
+
sdk: string;
|
|
129
|
+
};
|
|
130
|
+
export declare function normalizeOrigin(value: string): string;
|
|
131
|
+
/** Host header values a TLS-terminating proxy for `publicUrl` may present. */
|
|
132
|
+
export declare function publicHosts(config: ServeConfig): string[];
|
|
133
|
+
export declare function readServeConfig(root: string): Promise<ServeConfig | undefined>;
|
|
134
|
+
export declare function writeServeConfig(root: string, config: ServeConfig): Promise<void>;
|
|
135
|
+
/** The kit-generated Claude attachment: tealbrick MCP server plus the child guard hook. */
|
|
136
|
+
declare const generatedClaude: z.ZodObject<{
|
|
137
|
+
settings: z.ZodObject<{
|
|
138
|
+
hooks: z.ZodObject<{
|
|
139
|
+
PreToolUse: z.ZodArray<z.ZodObject<{
|
|
140
|
+
matcher: z.ZodLiteral<"mcp__tealbrick__.*">;
|
|
141
|
+
hooks: z.ZodArray<z.ZodObject<{
|
|
142
|
+
type: z.ZodLiteral<"command">;
|
|
143
|
+
command: z.ZodString;
|
|
144
|
+
args: z.ZodArray<z.ZodString>;
|
|
145
|
+
}, z.core.$strict>>;
|
|
146
|
+
}, z.core.$strict>>;
|
|
147
|
+
}, z.core.$strict>;
|
|
148
|
+
}, z.core.$strict>;
|
|
149
|
+
mcpServers: z.ZodObject<{
|
|
150
|
+
tealbrick: z.ZodObject<{
|
|
151
|
+
command: z.ZodString;
|
|
152
|
+
args: z.ZodArray<z.ZodString>;
|
|
153
|
+
}, z.core.$strict>;
|
|
154
|
+
}, z.core.$strict>;
|
|
155
|
+
}, z.core.$strict>;
|
|
156
|
+
export type ClaudeAttachment = z.infer<typeof generatedClaude>;
|
|
157
|
+
export declare function readClaudeAttachment(root: string): Promise<ClaudeAttachment>;
|
|
158
|
+
/** Resolve the effective Claude profile. Explicit serve fields override the imported recipe. */
|
|
159
|
+
export declare function resolveClaudeProfile(root: string, config: ServeConfig): Promise<ClaudeProfile>;
|
|
160
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { lstat, readFile, realpath, writeFile, rename, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
/** Operator configuration for `tealbrick native serve`. Contains no credentials. */
|
|
6
|
+
const absolute = z.string().min(1).refine(v => isAbsolute(v) && !v.includes('\0'), 'absolute path required');
|
|
7
|
+
export const hostPattern = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*(?::\d{1,5})?$/i;
|
|
8
|
+
const mcpTool = z.string().regex(/^mcp__[a-zA-Z0-9_-]+__[a-zA-Z0-9_-]+$/);
|
|
9
|
+
const tool = z.union([z.string().regex(/^[A-Z][A-Za-z]{1,40}$/), mcpTool]);
|
|
10
|
+
const serverName = z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/).refine(n => n !== 'tealbrick', 'the tealbrick MCP server comes from .tealbrick/native-sdk.json');
|
|
11
|
+
const stdio = z.object({ type: z.literal('stdio').optional(), command: z.string().min(1), args: z.array(z.string()).optional(), env: z.record(z.string(), z.string()).optional() }).strict();
|
|
12
|
+
export const tealbrickCallGrant = z.object({ registrationId: z.string().min(1), operation: z.string().min(1), inputEquals: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])).default({}) }).strict();
|
|
13
|
+
const effort = z.enum(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
14
|
+
/** Tools an agent may add to its default (Restricted) session access: research and expert delegation, never shell or writes. */
|
|
15
|
+
export const defaultAccessTools = ['WebSearch', 'WebFetch', 'Agent', 'TodoWrite'];
|
|
16
|
+
/** Tools an expert subagent may hold; workspace reads stay contained and network content stays untrusted. */
|
|
17
|
+
const expertTool = z.enum(['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch']);
|
|
18
|
+
const expertName = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/);
|
|
19
|
+
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
|
+
const claude = z.object({
|
|
21
|
+
/** Package directory or entry file of @anthropic-ai/claude-agent-sdk. Default: resolved from the kit root. */
|
|
22
|
+
sdkModule: absolute.optional(),
|
|
23
|
+
/** Working directory. Claude session transcripts are keyed by it; keep it stable. */
|
|
24
|
+
cwd: absolute,
|
|
25
|
+
instructionsFile: absolute.optional(),
|
|
26
|
+
/** Import model/limits/MCP/read tools/app-call ceilings from an existing tealbrick-native recipe file. */
|
|
27
|
+
recipe: z.object({ path: absolute, agent: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/) }).strict().optional(),
|
|
28
|
+
model: z.string().min(1).max(100).optional(),
|
|
29
|
+
effort: effort.optional(),
|
|
30
|
+
maxTurns: z.number().int().min(1).max(200).optional(),
|
|
31
|
+
maxBudgetUsd: z.number().positive().max(100).optional(),
|
|
32
|
+
timeoutSeconds: z.number().int().min(10).max(7200).optional(),
|
|
33
|
+
/** Extra tools in this agent's default (Restricted) session access. */
|
|
34
|
+
defaultTools: z.array(z.enum(defaultAccessTools)).max(4).optional(),
|
|
35
|
+
/** Expert subagents the Agent tool may delegate to, foreground only, inheriting the session model. */
|
|
36
|
+
experts: z.record(expertName, expert).optional(),
|
|
37
|
+
/** Tool allowlist for the opt-in "Native defaults" session access mode. */
|
|
38
|
+
nativeTools: z.array(tool).max(64).optional(),
|
|
39
|
+
/** Operator-vetted read-only MCP tools, also available in Restricted mode. */
|
|
40
|
+
readTools: z.array(mcpTool).max(32).optional(),
|
|
41
|
+
mcpServers: z.record(serverName, stdio).optional(),
|
|
42
|
+
tealbrickCalls: z.array(tealbrickCallGrant).max(64).optional(),
|
|
43
|
+
/** Refuse turns unless the SDK reports first-party subscription sign-in (no API-key fallback). */
|
|
44
|
+
requireSubscription: z.boolean().optional(),
|
|
45
|
+
/** 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(),
|
|
47
|
+
}).strict();
|
|
48
|
+
const codex = z.object({ codexBin: absolute, cwd: absolute }).strict();
|
|
49
|
+
export const serveConfigSchema = z.object({
|
|
50
|
+
version: z.literal(1),
|
|
51
|
+
port: z.number().int().min(1024).max(65535),
|
|
52
|
+
/** Public HTTPS origin registered on Portal (for example a tailscale serve URL). */
|
|
53
|
+
publicUrl: z.string().url().optional(),
|
|
54
|
+
/** Extra exact Host header values. The public URL host and loopback are added automatically. */
|
|
55
|
+
allowedHosts: z.array(z.string().regex(hostPattern)).max(8).optional(),
|
|
56
|
+
maxActiveTurns: z.number().int().min(1).max(16).optional(),
|
|
57
|
+
/** Written by `native enroll`: Portal identity used to verify every request. */
|
|
58
|
+
agent: z.object({ id: z.string().min(1), name: z.string().min(1).max(80), ownerId: z.string().min(1) }).strict().optional(),
|
|
59
|
+
claude: claude.optional(),
|
|
60
|
+
codex: codex.optional(),
|
|
61
|
+
}).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') });
|
|
63
|
+
async function regularFile(path, { ownerOnly }) {
|
|
64
|
+
const stat = await lstat(path);
|
|
65
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
66
|
+
throw Error('kit_native_serve_unsafe_file');
|
|
67
|
+
if (process.platform !== 'win32' && (stat.mode & (ownerOnly ? 0o077 : 0o022)))
|
|
68
|
+
throw Error('kit_native_serve_file_permissions');
|
|
69
|
+
}
|
|
70
|
+
/** Stable, value-free error codes for operator files; never echo file contents. */
|
|
71
|
+
function parsed(schema, text, code) {
|
|
72
|
+
let value;
|
|
73
|
+
try {
|
|
74
|
+
value = JSON.parse(text);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw Error(code);
|
|
78
|
+
}
|
|
79
|
+
const result = schema.safeParse(value);
|
|
80
|
+
if (!result.success)
|
|
81
|
+
throw Error(code + ':' + result.error.issues.map(i => i.path.join('.') || 'root').slice(0, 5).join(','));
|
|
82
|
+
return result.data;
|
|
83
|
+
}
|
|
84
|
+
export function normalizeOrigin(value) {
|
|
85
|
+
const url = new URL(value);
|
|
86
|
+
const loopback = ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname);
|
|
87
|
+
if (!(url.protocol === 'https:' || url.protocol === 'http:' && loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/')
|
|
88
|
+
throw Error('kit_native_public_url_https_origin_required');
|
|
89
|
+
return url.origin;
|
|
90
|
+
}
|
|
91
|
+
/** Host header values a TLS-terminating proxy for `publicUrl` may present. */
|
|
92
|
+
export function publicHosts(config) {
|
|
93
|
+
const hosts = [...(config.allowedHosts ?? [])];
|
|
94
|
+
if (config.publicUrl) {
|
|
95
|
+
const url = new URL(config.publicUrl);
|
|
96
|
+
hosts.push(url.host);
|
|
97
|
+
if (!url.port)
|
|
98
|
+
hosts.push(url.hostname + ':' + (url.protocol === 'https:' ? 443 : 80));
|
|
99
|
+
}
|
|
100
|
+
return [...new Set(hosts.map(h => h.toLowerCase()))];
|
|
101
|
+
}
|
|
102
|
+
export async function readServeConfig(root) {
|
|
103
|
+
const path = servePaths(root).config;
|
|
104
|
+
try {
|
|
105
|
+
await regularFile(path, { ownerOnly: true });
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
if (e.code === 'ENOENT')
|
|
109
|
+
return undefined;
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
const config = parsed(serveConfigSchema, await readFile(path, 'utf8'), 'kit_native_serve_config_invalid');
|
|
113
|
+
if (config.publicUrl && normalizeOrigin(config.publicUrl) !== config.publicUrl)
|
|
114
|
+
throw Error('kit_native_public_url_https_origin_required');
|
|
115
|
+
return config;
|
|
116
|
+
}
|
|
117
|
+
export async function writeServeConfig(root, config) {
|
|
118
|
+
const value = serveConfigSchema.parse(config), { config: path } = servePaths(root);
|
|
119
|
+
await mkdir(join(root, '.tealbrick'), { recursive: true, mode: 0o700 });
|
|
120
|
+
try {
|
|
121
|
+
if ((await lstat(path)).isSymbolicLink())
|
|
122
|
+
throw Error('kit_symlink_path');
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
if (e.code !== 'ENOENT')
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
const tmp = path + '.' + randomUUID();
|
|
129
|
+
await writeFile(tmp, JSON.stringify(value, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
130
|
+
await rename(tmp, path);
|
|
131
|
+
}
|
|
132
|
+
/** The kit-generated Claude attachment: tealbrick MCP server plus the child guard hook. */
|
|
133
|
+
const generatedClaude = z.object({
|
|
134
|
+
settings: z.object({ hooks: z.object({ PreToolUse: z.array(z.object({ matcher: z.literal('mcp__tealbrick__.*'), hooks: z.array(z.object({ type: z.literal('command'), command: z.string().min(1), args: z.array(z.string()).min(1) }).strict()).min(1) }).strict()).min(1) }).strict() }).strict(),
|
|
135
|
+
mcpServers: z.object({ tealbrick: z.object({ command: z.string().min(1), args: z.array(z.string()).min(1) }).strict() }).strict(),
|
|
136
|
+
}).strict();
|
|
137
|
+
export async function readClaudeAttachment(root) {
|
|
138
|
+
const path = servePaths(root).sdk;
|
|
139
|
+
await regularFile(path, { ownerOnly: true });
|
|
140
|
+
const value = parsed(generatedClaude, await readFile(path, 'utf8'), 'kit_native_sdk_config_invalid');
|
|
141
|
+
const args = value.mcpServers.tealbrick.args, at = args.indexOf('--root');
|
|
142
|
+
if (args.slice(-4, -2).join(' ') !== 'native mcp' || at < 0 || resolve(args[at + 1] ?? '') !== await realpath(root))
|
|
143
|
+
throw Error('kit_native_sdk_config_root_mismatch');
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
const recipeSchema = z.object({
|
|
147
|
+
model: z.string().min(1).optional(), effort: effort.optional(), maxTurns: z.number().int().min(1).max(200).optional(),
|
|
148
|
+
maxBudgetUsd: z.number().positive().max(100).optional(), timeoutSeconds: z.number().int().min(10).max(7200).optional(),
|
|
149
|
+
agents: z.record(z.string(), z.object({ mcpServers: z.record(serverName, stdio).optional(), readTools: z.array(mcpTool).max(32).optional(), tealbrickCalls: z.array(tealbrickCallGrant).max(64).optional(), tealbrickSdkConfig: absolute.optional() }).passthrough()),
|
|
150
|
+
}).passthrough();
|
|
151
|
+
/** Resolve the effective Claude profile. Explicit serve fields override the imported recipe. */
|
|
152
|
+
export async function resolveClaudeProfile(root, config) {
|
|
153
|
+
const c = config.claude;
|
|
154
|
+
if (!c)
|
|
155
|
+
throw Error('kit_native_serve_claude_profile_required');
|
|
156
|
+
let recipe, agent;
|
|
157
|
+
if (c.recipe) {
|
|
158
|
+
await regularFile(c.recipe.path, { ownerOnly: false });
|
|
159
|
+
recipe = parsed(recipeSchema, await readFile(c.recipe.path, 'utf8'), 'kit_native_serve_recipe_invalid');
|
|
160
|
+
agent = recipe.agents[c.recipe.agent];
|
|
161
|
+
if (!agent)
|
|
162
|
+
throw Error('kit_native_serve_recipe_agent_missing');
|
|
163
|
+
// The recipe must describe THIS kit root's Portal connection, never another agent's.
|
|
164
|
+
if (agent.tealbrickSdkConfig && await realpath(agent.tealbrickSdkConfig) !== await realpath(servePaths(root).sdk))
|
|
165
|
+
throw Error('kit_native_serve_recipe_connection_mismatch');
|
|
166
|
+
}
|
|
167
|
+
const cwd = resolve(c.cwd), protectedRoot = join(await realpath(root), '.tealbrick');
|
|
168
|
+
if (protectedRoot.startsWith(cwd + '/') || cwd === protectedRoot || cwd.startsWith(protectedRoot + '/'))
|
|
169
|
+
throw Error('kit_native_serve_cwd_contains_credentials');
|
|
170
|
+
if (!(await lstat(cwd).catch(() => undefined))?.isDirectory())
|
|
171
|
+
throw Error('kit_native_serve_cwd_missing');
|
|
172
|
+
const instructions = c.instructionsFile ? (await regularFile(c.instructionsFile, { ownerOnly: false }), await readFile(c.instructionsFile, 'utf8')) : undefined;
|
|
173
|
+
if (instructions && instructions.length > 200000)
|
|
174
|
+
throw Error('kit_native_serve_instructions_too_large');
|
|
175
|
+
const readTools = c.readTools ?? agent?.readTools ?? [];
|
|
176
|
+
const experts = {};
|
|
177
|
+
for (const [name, e] of Object.entries(c.experts ?? {})) {
|
|
178
|
+
await regularFile(e.promptFile, { ownerOnly: false });
|
|
179
|
+
const prompt = await readFile(e.promptFile, 'utf8');
|
|
180
|
+
if (!prompt.trim() || prompt.length > 50000)
|
|
181
|
+
throw Error('kit_native_serve_expert_prompt_invalid:' + name);
|
|
182
|
+
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
|
+
}
|
|
184
|
+
return { cwd, sdkModule: c.sdkModule, instructions, model: c.model ?? recipe?.model, effort: c.effort ?? recipe?.effort,
|
|
185
|
+
maxTurns: c.maxTurns ?? recipe?.maxTurns ?? 24, maxBudgetUsd: c.maxBudgetUsd ?? recipe?.maxBudgetUsd, timeoutSeconds: c.timeoutSeconds ?? recipe?.timeoutSeconds ?? 900,
|
|
186
|
+
defaultTools: [...new Set(c.defaultTools ?? [])], experts,
|
|
187
|
+
nativeTools: c.nativeTools ?? ['Read', 'Glob', 'Grep', 'TodoWrite'], readTools: readTools.filter(t => !t.startsWith('mcp__tealbrick__')),
|
|
188
|
+
mcpServers: c.mcpServers ?? agent?.mcpServers ?? {}, tealbrickCalls: c.tealbrickCalls ?? agent?.tealbrickCalls ?? [],
|
|
189
|
+
requireSubscription: c.requireSubscription ?? false, envPassthrough: c.envPassthrough ?? [] };
|
|
190
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type AuthResult } from '@tealbrick/portal';
|
|
2
|
+
import { type ClaudeSdk } from './native-claude.js';
|
|
3
|
+
import { type RuntimeSyncOptions } from './native-runtime-sync.js';
|
|
4
|
+
/** LangChain has no stable session/stream/cancel contract to drive from here yet. */
|
|
5
|
+
export declare function langchainHarness(): never;
|
|
6
|
+
export interface ServeDependencies {
|
|
7
|
+
authenticate?: (request: Request) => Promise<AuthResult>;
|
|
8
|
+
claudeSdk?: ClaudeSdk;
|
|
9
|
+
log?: (event: Record<string, unknown>) => void;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
/** Background Portal config/ack sync for this runtime; false disables it (tests only). */
|
|
12
|
+
runtimeSync?: RuntimeSyncOptions | false;
|
|
13
|
+
}
|
|
14
|
+
/** Build (but do not bind) the owner-authenticated chat endpoint for this kit root. */
|
|
15
|
+
export declare function createNativeServe(root: string, deps?: ServeDependencies): Promise<{
|
|
16
|
+
port: number;
|
|
17
|
+
harness: "claude" | "codex" | "langchain";
|
|
18
|
+
runtime: {
|
|
19
|
+
snapshot: () => import("./native-runtime-sync.js").RuntimeSyncSnapshot;
|
|
20
|
+
capabilities: () => {
|
|
21
|
+
revision: string;
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
apps: {
|
|
24
|
+
registrationId: string;
|
|
25
|
+
appId: "knowledge";
|
|
26
|
+
actions: import("@tealbrick/portal/runtime").CrudAction[];
|
|
27
|
+
available: boolean;
|
|
28
|
+
operations: import("@tealbrick/portal/runtime").KnowledgeOperation[];
|
|
29
|
+
}[];
|
|
30
|
+
} | undefined;
|
|
31
|
+
syncNow: () => Promise<void>;
|
|
32
|
+
stop(): Promise<void>;
|
|
33
|
+
} | undefined;
|
|
34
|
+
close: () => Promise<void>;
|
|
35
|
+
server: import("http").Server;
|
|
36
|
+
sessions: Map<string, import("@tealbrick/portal/native-bridge").BridgeSession<any>>;
|
|
37
|
+
}>;
|
|
38
|
+
export declare function serveNative(root: string): Promise<void>;
|
|
39
|
+
export declare function launchdLabel(root: string): string;
|
|
40
|
+
/** User LaunchAgent: restarts on crash, logs to the kit root's private state directory. */
|
|
41
|
+
export declare function launchdPlist(root: string, options?: {
|
|
42
|
+
node?: string;
|
|
43
|
+
cli?: string;
|
|
44
|
+
}): string;
|
|
45
|
+
type Exec = (file: string, args: string[]) => Promise<unknown>;
|
|
46
|
+
export declare function installLaunchd(root: string, options?: {
|
|
47
|
+
home?: string;
|
|
48
|
+
exec?: Exec;
|
|
49
|
+
load?: boolean;
|
|
50
|
+
uid?: number;
|
|
51
|
+
platform?: string;
|
|
52
|
+
}): Promise<{
|
|
53
|
+
installed: boolean;
|
|
54
|
+
label: string;
|
|
55
|
+
plist: string;
|
|
56
|
+
loaded: boolean;
|
|
57
|
+
logs: string;
|
|
58
|
+
}>;
|
|
59
|
+
export declare function uninstallLaunchd(root: string, options?: {
|
|
60
|
+
home?: string;
|
|
61
|
+
exec?: Exec;
|
|
62
|
+
uid?: number;
|
|
63
|
+
platform?: string;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
uninstalled: boolean;
|
|
66
|
+
label: string;
|
|
67
|
+
plist: string;
|
|
68
|
+
}>;
|
|
69
|
+
export {};
|