@tealbrick/kit 0.3.0-rc.5 → 0.3.0-rc.7
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 +2 -2
- package/README.md +16 -2
- package/dist/native-app-capabilities.d.ts +29 -0
- package/dist/native-app-capabilities.js +36 -0
- package/dist/native-child-guard.js +7 -2
- package/dist/native-claude.d.ts +17 -0
- package/dist/native-claude.js +116 -15
- package/dist/native-serve-config.d.ts +61 -0
- package/dist/native-serve-config.js +43 -1
- package/dist/native-serve.d.ts +4 -0
- package/dist/native-serve.js +15 -3
- package/dist/native.d.ts +1 -0
- package/dist/native.js +8 -1
- package/package.json +6 -6
package/BOOTSTRAP.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
> Prerelease 0.3.0-rc.
|
|
1
|
+
> Prerelease 0.3.0-rc.7 targets fresh Eve 0.70 installations. Use the explicit candidate version or `next` tag after publication. Scoped `@tealbrick/*` `latest` remains 0.2.7 for existing Eve 0.66 installations; durable cross-version session migration is not validated.
|
|
2
2
|
|
|
3
3
|
# Deployment guide: Eve agent + Teal Brick kit
|
|
4
4
|
|
|
@@ -19,7 +19,7 @@ For a fresh machine, the sequence is:
|
|
|
19
19
|
|
|
20
20
|
1. Install Node 24 and npm.
|
|
21
21
|
2. Create an Eve 0.70.0 project and configure its primary model credentials.
|
|
22
|
-
3. Install `@tealbrick/kit@0.3.0-rc.
|
|
22
|
+
3. Install `@tealbrick/kit@0.3.0-rc.7` inside that project.
|
|
23
23
|
4. Run `tealbrick setup` to sign in, select capabilities and register the card.
|
|
24
24
|
5. Build and start Eve, then check a real chat from desktop.
|
|
25
25
|
|
|
@@ -81,7 +81,7 @@ Run this inside the Eve project created in step 2 (or your existing compatible
|
|
|
81
81
|
project), not in an empty directory or as a global install:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
npm install --save-exact @tealbrick/kit@0.3.0-rc.
|
|
84
|
+
npm install --save-exact @tealbrick/kit@0.3.0-rc.7
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
This installs Portal, AVM, Voice, Vision, Deliver and the shared provider transport
|
package/NATIVE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Native agent setup — Stage A candidate
|
|
2
2
|
|
|
3
|
-
Prerelease suite **0.3.0-rc.
|
|
3
|
+
Prerelease suite **0.3.0-rc.7** targets the `next` tag. The seven scoped `@tealbrick/*` packages keep `latest` at 0.2.7, which lacks this flow; the unscoped `tealbrick` facade keeps `latest` at 0.3.0-rc.1. The `tealbrick` facade forwards the existing kit CLI; it is not another SDK. Matching Portal runtime setup endpoints must be deployed before native onboarding can succeed. RC4 added `tealbrick native serve` and `native enroll` (see README.md). RC5 keeps an idle serve process's runtime acknowledged in Portal, and lets the runtime reach Portal-provisioned Knowledge with short-lived Portal grants when no local app binding exists. RC6 adds the owner-only `/tealbrick/v1/capabilities` snapshot (Portal-granted apps and Marketplace consents) used by TBD Chat → Plugins, and `tealbrick native` without a subcommand prints usage. RC7 loads versioned expert contracts (`claude.contracts`) as SDK subagents with scope = contract ceiling ∩ owner grants, pauses approval operations for the owner's payload approval in TBD, and traces every delegation.
|
|
4
4
|
|
|
5
5
|
## Install and connect
|
|
6
6
|
|
|
7
|
-
Install with `npm install --save-exact tealbrick@0.3.0-rc.
|
|
7
|
+
Install with `npm install --save-exact tealbrick@0.3.0-rc.7` (or `tealbrick@next`) once registry publication is verified. Until then, use the review tarballs with the checksum-verifying `install-candidate.mjs`.
|
|
8
8
|
|
|
9
9
|
From your native agent workspace:
|
|
10
10
|
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
> Prerelease 0.3.0-rc.
|
|
1
|
+
> Prerelease 0.3.0-rc.7 targets fresh Eve 0.70 installations. Use the explicit candidate version or `next` tag after publication. Scoped `@tealbrick/*` `latest` remains 0.2.7 for existing Eve 0.66 installations; durable cross-version session migration is not validated.
|
|
2
2
|
|
|
3
3
|
# @tealbrick/kit
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ availability, not agent permissions. Nothing is mounted by installing the kit.
|
|
|
9
9
|
Install inside an existing Eve 0.70.0 project:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install --save-exact @tealbrick/kit@0.3.0-rc.
|
|
12
|
+
npm install --save-exact @tealbrick/kit@0.3.0-rc.7
|
|
13
13
|
npx --no-install tealbrick setup
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -139,6 +139,20 @@ stops the loop with one `native.runtime.sync.stopped` line. The per-turn
|
|
|
139
139
|
`Agent` delegates only to the configured `experts`, in the foreground with
|
|
140
140
|
the session model; each expert runs with its own `tools` (workspace reads,
|
|
141
141
|
`WebSearch`, `WebFetch`) and never receives Teal Brick tools.
|
|
142
|
+
- **Expert contracts** (`claude.contracts`: absolute paths to
|
|
143
|
+
`tealbrick.expert-contract/v1` JSON files, e.g. from the Agent-creation
|
|
144
|
+
cookbook) run as SDK subagents. Effective scope = contract ceiling ∩ the
|
|
145
|
+
owner's Portal grants ∩ the owner's `tealbrickCalls` ceiling; experts never
|
|
146
|
+
hold credentials or delegate. Payload approvals: `approvalOperations`
|
|
147
|
+
(default `marketplace_execute`) and a contract's outward writes pause the
|
|
148
|
+
exact call as an `input.requested` approval in TBD; decline or timeout denies.
|
|
149
|
+
Each delegation is traced to `.tealbrick/native-serve/traces/*.jsonl`
|
|
150
|
+
(owner, expert@version, brief, capabilities used, output digest).
|
|
151
|
+
- `GET /tealbrick/v1/capabilities` (same owner auth) reports what Portal
|
|
152
|
+
currently grants this agent: Knowledge apps from the background runtime sync
|
|
153
|
+
and, when `tealbrick native marketplace-enable` is set, Marketplace consents
|
|
154
|
+
(plugin and action names only; no credentials, leases or account
|
|
155
|
+
references). TBD uses it for Chat → Plugins.
|
|
142
156
|
- Elevation only through Desktop's session-access control (owner, explicit
|
|
143
157
|
confirmation, optimistic revision, no turn in flight). **Native defaults**
|
|
144
158
|
snapshots `nativeTools` (still workspace-contained); **Full** uses the
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RuntimeSync } from './native-runtime-sync.js';
|
|
2
|
+
export interface AppCapabilitiesOptions {
|
|
3
|
+
fetch?: typeof fetch;
|
|
4
|
+
now?: () => number;
|
|
5
|
+
marketplaceTtlMs?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Owner-facing snapshot of what Portal currently grants this agent: Knowledge apps from the background
|
|
9
|
+
* runtime sync, and Marketplace consents when Marketplace is activated in this kit root. Identifiers,
|
|
10
|
+
* actions and plugin/action names only; never credentials, leases or provider account references.
|
|
11
|
+
*/
|
|
12
|
+
export declare function appCapabilitiesProvider(root: string, runtime: () => RuntimeSync | undefined, options?: AppCapabilitiesOptions): () => Promise<{
|
|
13
|
+
schema: number;
|
|
14
|
+
sync: {
|
|
15
|
+
error?: string | undefined;
|
|
16
|
+
lastSyncAt?: number | undefined;
|
|
17
|
+
expiresAt?: number | undefined;
|
|
18
|
+
revision?: string | undefined;
|
|
19
|
+
status: string;
|
|
20
|
+
};
|
|
21
|
+
apps: {
|
|
22
|
+
registrationId: string;
|
|
23
|
+
appId: "knowledge";
|
|
24
|
+
actions: import("@tealbrick/portal/runtime").CrudAction[];
|
|
25
|
+
available: boolean;
|
|
26
|
+
operations: import("@tealbrick/portal/runtime").KnowledgeOperation[];
|
|
27
|
+
}[];
|
|
28
|
+
marketplace: Record<string, unknown>;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { MarketplaceRuntimeConnector } from '@tealbrick/portal/marketplace-runtime';
|
|
2
|
+
import { readKit } from './index.js';
|
|
3
|
+
import { runtimeSecret } from './secrets.js';
|
|
4
|
+
const code = (v) => typeof v === 'string' && /^[a-z][a-z0-9_]{0,80}$/.test(v) ? v : undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Owner-facing snapshot of what Portal currently grants this agent: Knowledge apps from the background
|
|
7
|
+
* runtime sync, and Marketplace consents when Marketplace is activated in this kit root. Identifiers,
|
|
8
|
+
* actions and plugin/action names only; never credentials, leases or provider account references.
|
|
9
|
+
*/
|
|
10
|
+
export function appCapabilitiesProvider(root, runtime, options = {}) {
|
|
11
|
+
const now = options.now ?? Date.now, ttl = options.marketplaceTtlMs ?? 30_000;
|
|
12
|
+
let cached;
|
|
13
|
+
async function marketplace() {
|
|
14
|
+
const kit = await readKit(root);
|
|
15
|
+
if (!kit.native?.active || !kit.native.marketplace || !kit.runtime)
|
|
16
|
+
return { enabled: false, status: 'inactive', grants: [] };
|
|
17
|
+
const { credentialRef, ...identity } = kit.runtime, key = JSON.stringify(kit.runtime);
|
|
18
|
+
if (cached && cached.key === key && now() - cached.at < ttl)
|
|
19
|
+
return cached.value;
|
|
20
|
+
let value;
|
|
21
|
+
try {
|
|
22
|
+
const consents = await new MarketplaceRuntimeConnector({ ...identity, credential: () => runtimeSecret(credentialRef, root), ...(options.fetch ? { fetch: options.fetch } : {}) }).discover();
|
|
23
|
+
value = { enabled: true, status: 'ok', grants: consents.map(c => ({ registrationId: `marketplace:${c.consentId}`, consentId: c.consentId, consentRevision: c.consentRevision, deploymentId: c.deploymentId, state: c.state, plugin: c.selection.pluginId, action: c.selection.actionKey, resourceKind: c.selection.resourceKind, operations: ['marketplace_execute'] })) };
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
value = { enabled: true, status: 'unavailable', error: code(e?.message) ?? 'runtime_marketplace_unavailable', grants: [] };
|
|
27
|
+
}
|
|
28
|
+
cached = { at: now(), key, value };
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
return async () => {
|
|
32
|
+
const snap = runtime()?.snapshot();
|
|
33
|
+
const apps = (snap?.capabilities?.apps ?? []).map(a => ({ registrationId: a.registrationId, appId: a.appId, actions: [...a.actions], available: a.available, operations: [...a.operations] }));
|
|
34
|
+
return { schema: 1, sync: { status: snap?.status ?? 'disabled', ...(snap?.revision ? { revision: snap.revision } : {}), ...(snap?.expiresAt ? { expiresAt: snap.expiresAt } : {}), ...(snap?.lastSyncAt ? { lastSyncAt: snap.lastSyncAt } : {}), ...(snap?.error ? { error: snap.error } : {}) }, apps, marketplace: await marketplace() };
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -9,8 +9,13 @@ try {
|
|
|
9
9
|
throw Error('oversized_hook');
|
|
10
10
|
}
|
|
11
11
|
const input = JSON.parse(text);
|
|
12
|
-
if (input.hook_event_name === 'PreToolUse' && typeof input.tool_name === 'string' && input.tool_name.startsWith('mcp__tealbrick__')
|
|
13
|
-
|
|
12
|
+
if (input.hook_event_name === 'PreToolUse' && typeof input.tool_name === 'string' && input.tool_name.startsWith('mcp__tealbrick__')) {
|
|
13
|
+
// Parent: no override. A child only if native serve admitted its contract type (non-secret env, SDK-authored agent_type);
|
|
14
|
+
// the in-process hook still checks contract ceiling ∩ owner grants on every call.
|
|
15
|
+
const admitted = (process.env.TB_CHILD_CONTRACTS ?? '').split(',').filter(Boolean);
|
|
16
|
+
if (input.agent_id === undefined || (typeof input.agent_type === 'string' && admitted.includes(input.agent_type)))
|
|
17
|
+
output = {};
|
|
18
|
+
}
|
|
14
19
|
}
|
|
15
20
|
catch { }
|
|
16
21
|
process.stdout.write(JSON.stringify(output) + '\n');
|
package/dist/native-claude.d.ts
CHANGED
|
@@ -25,11 +25,22 @@ export type ClaudeAccess = {
|
|
|
25
25
|
export declare const readOnlyTools: string[];
|
|
26
26
|
/** Removed from the model's context unless a wider access mode explicitly lists them. */
|
|
27
27
|
export declare const restrictedDenied: string[];
|
|
28
|
+
/** What a Portal registration id refers to; resolved from the runtime's own capability snapshot. */
|
|
29
|
+
export type AppRef = {
|
|
30
|
+
appId: string;
|
|
31
|
+
plugin?: string;
|
|
32
|
+
action?: string;
|
|
33
|
+
};
|
|
28
34
|
export interface ToolPolicy {
|
|
29
35
|
access: ClaudeAccess;
|
|
30
36
|
profile: ClaudeProfile;
|
|
31
37
|
protectedPaths: string[];
|
|
38
|
+
resolveApp?: (registrationId: string) => Promise<AppRef | undefined>;
|
|
32
39
|
}
|
|
40
|
+
/** Contract experts that may reach the owner's Teal Brick connector (still bounded per call by decideTool). */
|
|
41
|
+
export declare const contractsWithAppAccess: (profile: ClaudeProfile) => string[];
|
|
42
|
+
/** Payload-bound approval: owner-wide approval operations, plus a contract's own list and any outward write by a contract expert. */
|
|
43
|
+
export declare function needsApproval(profile: ClaudeProfile, name: string, input: Record<string, unknown>, agentType?: string): boolean;
|
|
33
44
|
/** Single decision point for PreToolUse hooks and canUseTool. Names and decisions only are logged. */
|
|
34
45
|
export declare function decideTool(policy: ToolPolicy, name: string, input: Record<string, unknown>, agentId?: string, agentType?: string): Promise<{
|
|
35
46
|
allow: boolean;
|
|
@@ -43,6 +54,12 @@ export interface ClaudeHarnessOptions {
|
|
|
43
54
|
protectedPaths: string[];
|
|
44
55
|
log?: (event: Record<string, unknown>) => void;
|
|
45
56
|
env?: NodeJS.ProcessEnv;
|
|
57
|
+
/** Resolves registration ids to apps for contract scope checks. */
|
|
58
|
+
resolveApp?: (registrationId: string) => Promise<AppRef | undefined>;
|
|
59
|
+
/** Owner-only directory for per-run delegation trace records (JSONL). */
|
|
60
|
+
traceDir?: string;
|
|
61
|
+
/** How long a payload approval may wait before it is denied. */
|
|
62
|
+
approvalTimeoutMs?: number;
|
|
46
63
|
}
|
|
47
64
|
/** Claude Agent SDK adapter. Claude's persisted session transcript is the source of truth;
|
|
48
65
|
* every turn is a fresh query() that resumes the same session id. */
|
package/dist/native-claude.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import { realpath } from 'node:fs/promises';
|
|
4
5
|
import { dirname, isAbsolute, join, relative, resolve, basename } from 'node:path';
|
|
@@ -42,12 +43,44 @@ async function contained(root, input, deny) {
|
|
|
42
43
|
const inside = (base) => { const rel = relative(base, path); return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); };
|
|
43
44
|
return inside(await realpath(root)) && !deny.some(inside);
|
|
44
45
|
}
|
|
46
|
+
const norm = (op) => op.replaceAll('.', '_');
|
|
47
|
+
const contractFor = (profile, agentType) => agentType !== undefined && Object.hasOwn(profile.contracts, agentType) ? profile.contracts[agentType] : undefined;
|
|
48
|
+
/** Contract experts that may reach the owner's Teal Brick connector (still bounded per call by decideTool). */
|
|
49
|
+
export const contractsWithAppAccess = (profile) => Object.values(profile.contracts).filter(c => c.capabilities.apps.length || c.capabilities.marketplace.length).map(c => c.id);
|
|
50
|
+
/** Payload-bound approval: owner-wide approval operations, plus a contract's own list and any outward write by a contract expert. */
|
|
51
|
+
export function needsApproval(profile, name, input, agentType) {
|
|
52
|
+
if (name !== 'mcp__tealbrick__tealbrick_call' || typeof input.operation !== 'string')
|
|
53
|
+
return false;
|
|
54
|
+
const op = norm(input.operation), contract = contractFor(profile, agentType);
|
|
55
|
+
if (profile.approvalOperations.some(o => norm(o) === op))
|
|
56
|
+
return true;
|
|
57
|
+
if (contract && ((op === 'marketplace_execute' && contract.approval.outwardWrites === 'always') || contract.approval.operations.some(o => norm(o) === op)))
|
|
58
|
+
return true;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
45
61
|
/** Single decision point for PreToolUse hooks and canUseTool. Names and decisions only are logged. */
|
|
46
62
|
export async function decideTool(policy, name, input, agentId, agentType) {
|
|
47
63
|
const { access, profile } = policy;
|
|
48
64
|
if (name.startsWith('mcp__tealbrick__')) {
|
|
49
|
-
if (agentId !== undefined)
|
|
50
|
-
|
|
65
|
+
if (agentId !== undefined) {
|
|
66
|
+
// A child reaches the owner's connector only under a contract, and only within contract ceiling ∩ owner grants.
|
|
67
|
+
const contract = contractFor(profile, agentType);
|
|
68
|
+
if (!contract || !(contract.capabilities.apps.length || contract.capabilities.marketplace.length))
|
|
69
|
+
return { allow: false, reason: 'Teal Brick authority is not delegated to child agents' };
|
|
70
|
+
if (name === 'mcp__tealbrick__tealbrick_call') {
|
|
71
|
+
const ref = typeof input.registrationId === 'string' ? await policy.resolveApp?.(input.registrationId) : undefined;
|
|
72
|
+
if (!ref)
|
|
73
|
+
return { allow: false, reason: 'Unknown app registration for this expert' };
|
|
74
|
+
if (input.operation === 'marketplace_execute') {
|
|
75
|
+
if (ref.appId !== 'marketplace' || !contract.capabilities.marketplace.some(m => m.plugin === ref.plugin && m.actions.includes(String(ref.action))))
|
|
76
|
+
return { allow: false, reason: 'Outside the expert contract' };
|
|
77
|
+
}
|
|
78
|
+
else if (!contract.capabilities.apps.some(a => a.appId === ref.appId && a.operations.includes(String(input.operation))))
|
|
79
|
+
return { allow: false, reason: 'Outside the expert contract' };
|
|
80
|
+
}
|
|
81
|
+
else if (name !== 'mcp__tealbrick__tealbrick_capabilities')
|
|
82
|
+
return { allow: false, reason: 'Outside the expert contract' };
|
|
83
|
+
}
|
|
51
84
|
if (name === 'mcp__tealbrick__tealbrick_call' && profile.tealbrickCalls.length) {
|
|
52
85
|
const args = input.input;
|
|
53
86
|
const ok = !Object.keys(input).some(k => !['registrationId', 'operation', 'input'].includes(k)) && !!args && typeof args === 'object' && !Array.isArray(args) && profile.tealbrickCalls.some(g => g.registrationId === input.registrationId && g.operation === input.operation && Object.entries(g.inputEquals).every(([k, v]) => Object.hasOwn(args, k) && args[k] === v));
|
|
@@ -64,11 +97,15 @@ export async function decideTool(policy, name, input, agentId, agentType) {
|
|
|
64
97
|
const allowed = access.mode === 'restricted' ? [...[...readOnlyTools, ...profile.defaultTools].filter(t => access.tools.includes(t)), ...profile.readTools] : [...access.tools, ...profile.readTools];
|
|
65
98
|
if (!allowed.includes(name))
|
|
66
99
|
return { allow: false, reason: `Not available in ${access.mode} session access` };
|
|
67
|
-
if (agentId !== undefined
|
|
68
|
-
|
|
100
|
+
if (agentId !== undefined) {
|
|
101
|
+
const contract = contractFor(profile, agentType);
|
|
102
|
+
const tools = contract ? contract.capabilities.builtinTools : (agentType !== undefined && Object.hasOwn(profile.experts, agentType) ? [profile.experts[agentType]] : Object.values(profile.experts)).flatMap(e => e.tools);
|
|
103
|
+
if (!tools.includes(name))
|
|
104
|
+
return { allow: false, reason: 'Not available to expert agents' };
|
|
105
|
+
}
|
|
69
106
|
if (name === 'Agent') {
|
|
70
107
|
const type = String(input.subagent_type ?? '');
|
|
71
|
-
if (!Object.hasOwn(profile.experts, type) || input.run_in_background || input.isolation || input.resume || (input.model !== undefined && input.model !== 'inherit'))
|
|
108
|
+
if (!(Object.hasOwn(profile.experts, type) || Object.hasOwn(profile.contracts, type)) || input.run_in_background || input.isolation || input.resume || (input.model !== undefined && input.model !== 'inherit'))
|
|
72
109
|
return { allow: false, reason: 'Only configured experts may be delegated, in the foreground' };
|
|
73
110
|
}
|
|
74
111
|
const fields = pathTools[name];
|
|
@@ -104,26 +141,85 @@ export function claudeHarness(options) {
|
|
|
104
141
|
const emit = (event) => { for (const listener of listeners)
|
|
105
142
|
listener(event); };
|
|
106
143
|
const restricted = () => ({ mode: 'restricted', revision: 0, tools: [...readOnlyTools, ...profile.defaultTools] });
|
|
107
|
-
const experts = Object.keys(profile.experts);
|
|
108
|
-
const
|
|
144
|
+
const experts = [...Object.keys(profile.experts), ...Object.keys(profile.contracts)];
|
|
145
|
+
const appContracts = contractsWithAppAccess(profile);
|
|
146
|
+
const agents = Object.fromEntries([
|
|
147
|
+
...Object.entries(profile.experts).map(([name, e]) => [name, { description: e.description, prompt: e.prompt, tools: e.tools, model: 'inherit', maxTurns: e.maxTurns, ...(e.effort ? { effort: e.effort } : {}), omitClaudeMd: true }]),
|
|
148
|
+
...Object.values(profile.contracts).map(c => [c.id, { description: `${c.purpose} (contract ${c.id}@${c.version})`,
|
|
149
|
+
prompt: `${c.instructions}\n\nYou are the ${c.id} expert (contract ${c.id}@${c.version}). The brief you receive should match this JSON schema:\n\`\`\`json\n${JSON.stringify(c.input)}\n\`\`\`\nEnd your final message with one fenced json block that matches this output schema:\n\`\`\`json\n${JSON.stringify(c.output)}\n\`\`\`\nYou hold no credentials and cannot delegate. ${c.approval.outwardWrites === 'always' ? 'Any outward write pauses for the owner\'s approval of the exact payload; report a declined approval as blocked.' : 'Do not attempt outward writes.'}`,
|
|
150
|
+
tools: [...c.capabilities.builtinTools, ...(appContracts.includes(c.id) ? ['mcp__tealbrick__tealbrick_capabilities', 'mcp__tealbrick__tealbrick_call'] : [])],
|
|
151
|
+
model: c.model, maxTurns: c.budget.maxTurns, ...(c.effort ? { effort: c.effort } : {}), omitClaudeMd: true }]),
|
|
152
|
+
]);
|
|
153
|
+
// Payload approvals waiting on the owner (bridge input.requested → respond).
|
|
154
|
+
const approvals = new Map();
|
|
155
|
+
const approvalTimeout = options.approvalTimeoutMs ?? 15 * 60_000;
|
|
156
|
+
const requestApproval = (sessionId, prompt) => new Promise(resolve => {
|
|
157
|
+
const requestId = randomUUID(), timer = setTimeout(() => { approvals.delete(requestId); resolve(false); }, approvalTimeout);
|
|
158
|
+
timer.unref?.();
|
|
159
|
+
approvals.set(requestId, { resolve, timer });
|
|
160
|
+
emit({ type: 'input.requested', sessionId, requestId, prompt });
|
|
161
|
+
});
|
|
162
|
+
const trace = async (record) => {
|
|
163
|
+
if (!options.traceDir)
|
|
164
|
+
return;
|
|
165
|
+
try {
|
|
166
|
+
await mkdir(options.traceDir, { recursive: true, mode: 0o700 });
|
|
167
|
+
await appendFile(join(options.traceDir, new Date().toISOString().slice(0, 10) + '.jsonl'), JSON.stringify({ ts: new Date().toISOString(), owner: options.name, ...record }) + '\n', { mode: 0o600 });
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
log({ event: 'claude.trace.failed' });
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const digest = (v) => createHash('sha256').update(typeof v === 'string' ? v : JSON.stringify(v ?? null)).digest('hex').slice(0, 16);
|
|
174
|
+
const clip = (v, n) => { const t = typeof v === 'string' ? v : JSON.stringify(v ?? null); return t.length > n ? t.slice(0, n) + '…' : t; };
|
|
175
|
+
const expertRef = (type) => { const c = type !== undefined && Object.hasOwn(profile.contracts, type) ? profile.contracts[type] : undefined; return c ? `${c.id}@${c.version}` : type; };
|
|
109
176
|
const childEnv = () => {
|
|
110
177
|
const out = { CLAUDE_AGENT_SDK_CLIENT_APP: 'tealbrick-kit/native-serve' };
|
|
178
|
+
// Non-secret: lets the generated child guard admit contract experts; decideTool still bounds every call.
|
|
179
|
+
if (appContracts.length)
|
|
180
|
+
out.TB_CHILD_CONTRACTS = appContracts.join(',');
|
|
111
181
|
for (const name of [...safeEnvNames, ...profile.envPassthrough])
|
|
112
182
|
if (env[name] && !name.startsWith('TEALBRICK_'))
|
|
113
183
|
out[name] = env[name];
|
|
114
184
|
return out;
|
|
115
185
|
};
|
|
116
186
|
const optionsFor = (sessionId, access, abort) => {
|
|
117
|
-
const policy = { access, profile, protectedPaths: options.protectedPaths };
|
|
187
|
+
const policy = { access, profile, protectedPaths: options.protectedPaths, ...(options.resolveApp ? { resolveApp: options.resolveApp } : {}) };
|
|
118
188
|
const builtins = access.tools.filter(t => !t.startsWith('mcp__'));
|
|
119
189
|
const servers = access.mode === 'restricted' ? Object.fromEntries(Object.entries(profile.mcpServers).filter(([name]) => profile.readTools.some(t => t.startsWith(`mcp__${name}__`)))) : profile.mcpServers;
|
|
120
190
|
const hook = async (input) => {
|
|
121
191
|
if (input?.hook_event_name !== 'PreToolUse')
|
|
122
192
|
return {};
|
|
123
|
-
const
|
|
124
|
-
|
|
193
|
+
const name = String(input.tool_name), args = input.tool_input ?? {}, child = input.agent_id !== undefined;
|
|
194
|
+
let decision = await decideTool(policy, name, args, input.agent_id, input.agent_type);
|
|
195
|
+
let approval;
|
|
196
|
+
if (decision.allow && needsApproval(profile, name, args, input.agent_type)) {
|
|
197
|
+
const who = child ? `expert ${expertRef(input.agent_type)}` : options.name;
|
|
198
|
+
const ok = await requestApproval(sessionId, `Approve ${who}: ${String(args.operation)} on ${String(args.registrationId)}\n${clip(args.input, 4000)}`);
|
|
199
|
+
approval = ok ? 'approved' : 'declined';
|
|
200
|
+
if (!ok)
|
|
201
|
+
decision = { allow: false, reason: 'The owner declined (or did not approve in time) this exact payload' };
|
|
202
|
+
}
|
|
203
|
+
log({ event: 'claude.tool', sessionId, tool: name.slice(0, 120), allowed: decision.allow, child, ...(approval ? { approval } : {}) });
|
|
204
|
+
if (child)
|
|
205
|
+
void trace({ event: 'expert.tool', sessionId, agentId: input.agent_id, expert: expertRef(input.agent_type), tool: name.slice(0, 120),
|
|
206
|
+
...(name === 'mcp__tealbrick__tealbrick_call' ? { registrationId: String(args.registrationId ?? ''), operation: String(args.operation ?? '') } : {}), allowed: decision.allow, ...(approval ? { approval } : {}) });
|
|
207
|
+
else if ((name === 'Agent' || name === 'Task') && decision.allow)
|
|
208
|
+
void trace({ event: 'delegation.start', sessionId, toolUseId: input.tool_use_id, expert: expertRef(String(args.subagent_type ?? '')), brief: clip(args.prompt, 2000), briefSha: digest(args.prompt) });
|
|
209
|
+
else if (approval)
|
|
210
|
+
void trace({ event: 'owner.approval', sessionId, operation: String(args.operation ?? ''), registrationId: String(args.registrationId ?? ''), approval });
|
|
125
211
|
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: decision.allow ? 'allow' : 'deny', permissionDecisionReason: decision.reason } };
|
|
126
212
|
};
|
|
213
|
+
const lifecycle = async (input) => {
|
|
214
|
+
const e = input?.hook_event_name;
|
|
215
|
+
if (e === 'SubagentStart')
|
|
216
|
+
void trace({ event: 'expert.start', sessionId, agentId: input.agent_id, expert: expertRef(input.agent_type) });
|
|
217
|
+
else if (e === 'SubagentStop')
|
|
218
|
+
void trace({ event: 'expert.stop', sessionId, agentId: input.agent_id, expert: expertRef(input.agent_type), output: clip(input.last_assistant_message, 4000), outputSha: digest(input.last_assistant_message) });
|
|
219
|
+
else if (e === 'PostToolUse' && (input.tool_name === 'Agent' || input.tool_name === 'Task') && input.agent_id === undefined)
|
|
220
|
+
void trace({ event: 'delegation.end', sessionId, toolUseId: input.tool_use_id, outputSha: digest(input.tool_response), outputChars: clip(input.tool_response, 1e9).length });
|
|
221
|
+
return {};
|
|
222
|
+
};
|
|
127
223
|
const append = [profile.instructions?.trim(), `You are ${options.name}, chatting with your owner through TBD. ${accessNote(access, experts)}`].filter(Boolean).join('\n\n');
|
|
128
224
|
const delegates = experts.length > 0 && (access.mode === 'full' || builtins.includes('Agent'));
|
|
129
225
|
return {
|
|
@@ -140,7 +236,7 @@ export function claudeHarness(options) {
|
|
|
140
236
|
...(access.mode === 'full' ? { allowDangerouslySkipPermissions: true } : {
|
|
141
237
|
canUseTool: async (name, input) => { const d = await decideTool(policy, name, input); return d.allow ? { behavior: 'allow', updatedInput: input } : { behavior: 'deny', message: d.reason }; },
|
|
142
238
|
}),
|
|
143
|
-
hooks: { PreToolUse: [{ hooks: [hook] }] },
|
|
239
|
+
hooks: { PreToolUse: [{ hooks: [hook] }], SubagentStart: [{ hooks: [lifecycle] }], SubagentStop: [{ hooks: [lifecycle] }], PostToolUse: [{ hooks: [lifecycle] }] },
|
|
144
240
|
systemPrompt: { type: 'preset', preset: 'claude_code', append },
|
|
145
241
|
persistSession: true, includePartialMessages: true,
|
|
146
242
|
...(started.has(sessionId) ? { resume: sessionId } : { sessionId }),
|
|
@@ -242,10 +338,10 @@ export function claudeHarness(options) {
|
|
|
242
338
|
log({ event: 'claude.turn.failed', sessionId, code: failure.code });
|
|
243
339
|
emit({ type: 'turn.ended', sessionId, status: 'failed', code: failure.code, message: failure.message, turnId: turn.turnId });
|
|
244
340
|
}
|
|
245
|
-
const capabilities = ['chat', 'resume', 'cancel', 'mcp', 'session-access'];
|
|
341
|
+
const capabilities = ['chat', 'resume', 'cancel', 'mcp', 'session-access', 'approvals', ...(experts.length ? ['subagents'] : [])];
|
|
246
342
|
return {
|
|
247
343
|
info: { harness: 'claude', protocol: 'tealbrick-native', transport: 'claude-agent-sdk', workflowId: 'claude-agent-sdk', ...(profile.model ? { model: profile.model } : {}), capabilities,
|
|
248
|
-
unsupported: ['eve-info', 'eve-workflows', 'eve-schedules', 'file-attachments', '
|
|
344
|
+
unsupported: ['eve-info', 'eve-workflows', 'eve-schedules', 'file-attachments', 'session-reset', 'session-clear', 'session-compact', 'client-context', 'output-schema'] },
|
|
249
345
|
restricted,
|
|
250
346
|
subscribe: listener => { listeners.add(listener); },
|
|
251
347
|
async create() { return randomUUID(); },
|
|
@@ -279,7 +375,12 @@ export function claudeHarness(options) {
|
|
|
279
375
|
return { mode, revision, tools: [...profile.nativeTools] };
|
|
280
376
|
},
|
|
281
377
|
},
|
|
282
|
-
|
|
378
|
+
respond(requestId, optionId) { const p = approvals.get(requestId); if (!p)
|
|
379
|
+
return; approvals.delete(requestId); clearTimeout(p.timer); p.resolve(optionId === 'accept'); },
|
|
380
|
+
async close() { for (const p of approvals.values()) {
|
|
381
|
+
clearTimeout(p.timer);
|
|
382
|
+
p.resolve(false);
|
|
383
|
+
} approvals.clear(); for (const turn of turns.values()) {
|
|
283
384
|
turn.cancelled = true;
|
|
284
385
|
turn.abort?.abort();
|
|
285
386
|
} },
|
|
@@ -20,6 +20,63 @@ declare const effort: z.ZodEnum<{
|
|
|
20
20
|
}>;
|
|
21
21
|
/** Tools an agent may add to its default (Restricted) session access: research and expert delegation, never shell or writes. */
|
|
22
22
|
export declare const defaultAccessTools: readonly ["WebSearch", "WebFetch", "Agent", "TodoWrite"];
|
|
23
|
+
export declare const expertContractSchema: z.ZodObject<{
|
|
24
|
+
schema: z.ZodLiteral<"tealbrick.expert-contract/v1">;
|
|
25
|
+
id: z.ZodString;
|
|
26
|
+
version: z.ZodString;
|
|
27
|
+
purpose: z.ZodString;
|
|
28
|
+
instructions: z.ZodString;
|
|
29
|
+
input: z.ZodObject<{
|
|
30
|
+
type: z.ZodLiteral<"object">;
|
|
31
|
+
}, z.core.$loose>;
|
|
32
|
+
output: z.ZodObject<{
|
|
33
|
+
type: z.ZodLiteral<"object">;
|
|
34
|
+
}, z.core.$loose>;
|
|
35
|
+
capabilities: z.ZodObject<{
|
|
36
|
+
builtinTools: z.ZodArray<z.ZodEnum<{
|
|
37
|
+
WebSearch: "WebSearch";
|
|
38
|
+
WebFetch: "WebFetch";
|
|
39
|
+
Read: "Read";
|
|
40
|
+
Glob: "Glob";
|
|
41
|
+
Grep: "Grep";
|
|
42
|
+
}>>;
|
|
43
|
+
apps: z.ZodArray<z.ZodObject<{
|
|
44
|
+
appId: z.ZodString;
|
|
45
|
+
operations: z.ZodArray<z.ZodString>;
|
|
46
|
+
}, z.core.$strict>>;
|
|
47
|
+
marketplace: z.ZodArray<z.ZodObject<{
|
|
48
|
+
plugin: z.ZodString;
|
|
49
|
+
actions: z.ZodArray<z.ZodString>;
|
|
50
|
+
}, z.core.$strict>>;
|
|
51
|
+
}, z.core.$strict>;
|
|
52
|
+
approval: z.ZodObject<{
|
|
53
|
+
outwardWrites: z.ZodEnum<{
|
|
54
|
+
always: "always";
|
|
55
|
+
"never-allowed": "never-allowed";
|
|
56
|
+
}>;
|
|
57
|
+
operations: z.ZodArray<z.ZodString>;
|
|
58
|
+
}, z.core.$strict>;
|
|
59
|
+
model: z.ZodEnum<{
|
|
60
|
+
haiku: "haiku";
|
|
61
|
+
inherit: "inherit";
|
|
62
|
+
opus: "opus";
|
|
63
|
+
sonnet: "sonnet";
|
|
64
|
+
}>;
|
|
65
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
66
|
+
low: "low";
|
|
67
|
+
high: "high";
|
|
68
|
+
medium: "medium";
|
|
69
|
+
}>>;
|
|
70
|
+
budget: z.ZodObject<{
|
|
71
|
+
maxTurns: z.ZodNumber;
|
|
72
|
+
}, z.core.$strict>;
|
|
73
|
+
runtime: z.ZodEnum<{
|
|
74
|
+
subagent: "subagent";
|
|
75
|
+
"desktop-operator": "desktop-operator";
|
|
76
|
+
}>;
|
|
77
|
+
changelog: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
78
|
+
}, z.core.$strict>;
|
|
79
|
+
export type ExpertContract = z.infer<typeof expertContractSchema>;
|
|
23
80
|
export declare const serveConfigSchema: z.ZodObject<{
|
|
24
81
|
version: z.ZodLiteral<1>;
|
|
25
82
|
port: z.ZodNumber;
|
|
@@ -75,6 +132,8 @@ export declare const serveConfigSchema: z.ZodObject<{
|
|
|
75
132
|
max: "max";
|
|
76
133
|
}>>;
|
|
77
134
|
}, z.core.$strict>>>;
|
|
135
|
+
contracts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
136
|
+
approvalOperations: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
78
137
|
nativeTools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>>;
|
|
79
138
|
readTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
80
139
|
mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -108,6 +167,8 @@ export interface ClaudeProfile {
|
|
|
108
167
|
timeoutSeconds: number;
|
|
109
168
|
defaultTools: string[];
|
|
110
169
|
experts: Record<string, ClaudeExpert>;
|
|
170
|
+
contracts: Record<string, ExpertContract>;
|
|
171
|
+
approvalOperations: string[];
|
|
111
172
|
nativeTools: string[];
|
|
112
173
|
readTools: string[];
|
|
113
174
|
mcpServers: Record<string, z.infer<typeof stdio>>;
|
|
@@ -16,6 +16,28 @@ export const defaultAccessTools = ['WebSearch', 'WebFetch', 'Agent', 'TodoWrite'
|
|
|
16
16
|
/** Tools an expert subagent may hold; workspace reads stay contained and network content stays untrusted. */
|
|
17
17
|
const expertTool = z.enum(['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch']);
|
|
18
18
|
const expertName = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/);
|
|
19
|
+
/** Versioned expert contract (tealbrick.expert-contract/v1). The contract is a ceiling; Portal grants still apply. */
|
|
20
|
+
const opName = z.string().regex(/^[a-z][a-z0-9_.]{1,120}$/);
|
|
21
|
+
export const expertContractSchema = z.object({
|
|
22
|
+
schema: z.literal('tealbrick.expert-contract/v1'),
|
|
23
|
+
id: z.string().regex(/^[a-z][a-z0-9-]{1,62}$/),
|
|
24
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
25
|
+
purpose: z.string().min(10).max(300),
|
|
26
|
+
instructions: z.string().min(40).max(8000),
|
|
27
|
+
input: z.object({ type: z.literal('object') }).passthrough(),
|
|
28
|
+
output: z.object({ type: z.literal('object') }).passthrough(),
|
|
29
|
+
capabilities: z.object({
|
|
30
|
+
builtinTools: z.array(expertTool).max(8),
|
|
31
|
+
apps: z.array(z.object({ appId: z.string().regex(/^[a-z][a-z0-9-]{1,40}$/), operations: z.array(z.string().regex(/^[a-z][a-z0-9_]{1,80}$/)).min(1) }).strict()).max(12),
|
|
32
|
+
marketplace: z.array(z.object({ plugin: z.string().regex(/^[a-z0-9][a-z0-9-]{1,80}$/), actions: z.array(z.string().regex(/^[a-z0-9][a-z0-9._-]{1,120}$/)).min(1) }).strict()).max(12),
|
|
33
|
+
}).strict(),
|
|
34
|
+
approval: z.object({ outwardWrites: z.enum(['always', 'never-allowed']), operations: z.array(opName) }).strict(),
|
|
35
|
+
model: z.enum(['inherit', 'opus', 'sonnet', 'haiku']),
|
|
36
|
+
effort: z.enum(['low', 'medium', 'high']).optional(),
|
|
37
|
+
budget: z.object({ maxTurns: z.number().int().min(1).max(50) }).strict(),
|
|
38
|
+
runtime: z.enum(['subagent', 'desktop-operator']),
|
|
39
|
+
changelog: z.array(z.string().max(300)).optional(),
|
|
40
|
+
}).strict();
|
|
19
41
|
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
42
|
const claude = z.object({
|
|
21
43
|
/** Package directory or entry file of @anthropic-ai/claude-agent-sdk. Default: resolved from the kit root. */
|
|
@@ -34,6 +56,10 @@ const claude = z.object({
|
|
|
34
56
|
defaultTools: z.array(z.enum(defaultAccessTools)).max(4).optional(),
|
|
35
57
|
/** Expert subagents the Agent tool may delegate to, foreground only, inheriting the session model. */
|
|
36
58
|
experts: z.record(expertName, expert).optional(),
|
|
59
|
+
/** Versioned expert contracts (JSON files) run as SDK subagents. Effective scope = contract ceiling ∩ owner grants. */
|
|
60
|
+
contracts: z.array(absolute).max(32).optional(),
|
|
61
|
+
/** Operations that always pause for the owner's approval of the exact payload, whoever calls them. */
|
|
62
|
+
approvalOperations: z.array(opName).max(64).optional(),
|
|
37
63
|
/** Tool allowlist for the opt-in "Native defaults" session access mode. */
|
|
38
64
|
nativeTools: z.array(tool).max(64).optional(),
|
|
39
65
|
/** Operator-vetted read-only MCP tools, also available in Restricted mode. */
|
|
@@ -173,6 +199,19 @@ export async function resolveClaudeProfile(root, config) {
|
|
|
173
199
|
if (instructions && instructions.length > 200000)
|
|
174
200
|
throw Error('kit_native_serve_instructions_too_large');
|
|
175
201
|
const readTools = c.readTools ?? agent?.readTools ?? [];
|
|
202
|
+
const contracts = {};
|
|
203
|
+
for (const path of c.contracts ?? []) {
|
|
204
|
+
await regularFile(path, { ownerOnly: false });
|
|
205
|
+
const text = await readFile(path, 'utf8');
|
|
206
|
+
if (text.length > 65536)
|
|
207
|
+
throw Error('kit_native_serve_contract_too_large');
|
|
208
|
+
const contract = parsed(expertContractSchema, text, 'kit_native_serve_contract_invalid');
|
|
209
|
+
if (contract.runtime !== 'subagent')
|
|
210
|
+
throw Error('kit_native_serve_contract_runtime_unsupported:' + contract.id);
|
|
211
|
+
if (Object.hasOwn(contracts, contract.id))
|
|
212
|
+
throw Error('kit_native_serve_contract_duplicate:' + contract.id);
|
|
213
|
+
contracts[contract.id] = contract;
|
|
214
|
+
}
|
|
176
215
|
const experts = {};
|
|
177
216
|
for (const [name, e] of Object.entries(c.experts ?? {})) {
|
|
178
217
|
await regularFile(e.promptFile, { ownerOnly: false });
|
|
@@ -181,9 +220,12 @@ export async function resolveClaudeProfile(root, config) {
|
|
|
181
220
|
throw Error('kit_native_serve_expert_prompt_invalid:' + name);
|
|
182
221
|
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
222
|
}
|
|
223
|
+
for (const id of Object.keys(contracts))
|
|
224
|
+
if (Object.hasOwn(experts, id))
|
|
225
|
+
throw Error('kit_native_serve_contract_duplicate:' + id);
|
|
184
226
|
return { cwd, sdkModule: c.sdkModule, instructions, model: c.model ?? recipe?.model, effort: c.effort ?? recipe?.effort,
|
|
185
227
|
maxTurns: c.maxTurns ?? recipe?.maxTurns ?? 24, maxBudgetUsd: c.maxBudgetUsd ?? recipe?.maxBudgetUsd, timeoutSeconds: c.timeoutSeconds ?? recipe?.timeoutSeconds ?? 900,
|
|
186
|
-
defaultTools: [...new Set(c.defaultTools ?? [])], experts,
|
|
228
|
+
defaultTools: [...new Set(c.defaultTools ?? [])], experts, contracts, approvalOperations: [...new Set(c.approvalOperations ?? ['marketplace_execute'])],
|
|
187
229
|
nativeTools: c.nativeTools ?? ['Read', 'Glob', 'Grep', 'TodoWrite'], readTools: readTools.filter(t => !t.startsWith('mcp__tealbrick__')),
|
|
188
230
|
mcpServers: c.mcpServers ?? agent?.mcpServers ?? {}, tealbrickCalls: c.tealbrickCalls ?? agent?.tealbrickCalls ?? [],
|
|
189
231
|
requireSubscription: c.requireSubscription ?? false, envPassthrough: c.envPassthrough ?? [] };
|
package/dist/native-serve.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AuthResult } from '@tealbrick/portal';
|
|
2
2
|
import { type ClaudeSdk } from './native-claude.js';
|
|
3
3
|
import { type RuntimeSyncOptions } from './native-runtime-sync.js';
|
|
4
|
+
import { type AppCapabilitiesOptions } from './native-app-capabilities.js';
|
|
4
5
|
/** LangChain has no stable session/stream/cancel contract to drive from here yet. */
|
|
5
6
|
export declare function langchainHarness(): never;
|
|
6
7
|
export interface ServeDependencies {
|
|
@@ -10,6 +11,9 @@ export interface ServeDependencies {
|
|
|
10
11
|
env?: NodeJS.ProcessEnv;
|
|
11
12
|
/** Background Portal config/ack sync for this runtime; false disables it (tests only). */
|
|
12
13
|
runtimeSync?: RuntimeSyncOptions | false;
|
|
14
|
+
/** Test seams for the owner-only /tealbrick/v1/capabilities snapshot. */
|
|
15
|
+
appCapabilities?: AppCapabilitiesOptions;
|
|
16
|
+
approvalTimeoutMs?: number;
|
|
13
17
|
}
|
|
14
18
|
/** Build (but do not bind) the owner-authenticated chat endpoint for this kit root. */
|
|
15
19
|
export declare function createNativeServe(root: string, deps?: ServeDependencies): Promise<{
|
package/dist/native-serve.js
CHANGED
|
@@ -12,6 +12,7 @@ import { readKit } from './index.js';
|
|
|
12
12
|
import { nativeSdkConfig } from './native-setup.js';
|
|
13
13
|
import { claudeHarness, loadClaudeSdk } from './native-claude.js';
|
|
14
14
|
import { startRuntimeSync } from './native-runtime-sync.js';
|
|
15
|
+
import { appCapabilitiesProvider } from './native-app-capabilities.js';
|
|
15
16
|
import { publicHosts, readClaudeAttachment, readServeConfig, resolveClaudeProfile, servePaths } from './native-serve-config.js';
|
|
16
17
|
/** LangChain has no stable session/stream/cancel contract to drive from here yet. */
|
|
17
18
|
export function langchainHarness() { throw Error('kit_native_serve_langchain_unsupported'); }
|
|
@@ -34,9 +35,20 @@ export async function createNativeServe(root, deps = {}) {
|
|
|
34
35
|
await mkdir(paths.state, { recursive: true, mode: 0o700 });
|
|
35
36
|
let adapter;
|
|
36
37
|
let codex;
|
|
38
|
+
let runtime;
|
|
39
|
+
const appCapabilities = appCapabilitiesProvider(root, () => runtime, deps.appCapabilities);
|
|
37
40
|
if (kit.native.harness === 'claude') {
|
|
38
41
|
const profile = await resolveClaudeProfile(root, config);
|
|
39
|
-
|
|
42
|
+
// Contract scope checks resolve registration ids from this runtime's own Portal-granted snapshot.
|
|
43
|
+
const resolveApp = async (registrationId) => {
|
|
44
|
+
const snap = await appCapabilities();
|
|
45
|
+
const app = snap.apps.find(a => a.registrationId === registrationId);
|
|
46
|
+
if (app)
|
|
47
|
+
return { appId: app.appId };
|
|
48
|
+
const grant = snap.marketplace.grants.find(g => g.registrationId === registrationId);
|
|
49
|
+
return grant ? { appId: 'marketplace', plugin: grant.plugin, action: grant.action } : undefined;
|
|
50
|
+
};
|
|
51
|
+
adapter = claudeHarness({ name: config.agent.name, profile, attachment: await readClaudeAttachment(root), sdk: deps.claudeSdk ?? await loadClaudeSdk(root, profile.sdkModule), protectedPaths: [join(root, '.tealbrick')], log, env: deps.env, resolveApp, traceDir: join(paths.state, 'traces'), ...(deps.approvalTimeoutMs ? { approvalTimeoutMs: deps.approvalTimeoutMs } : {}) });
|
|
40
52
|
}
|
|
41
53
|
else if (kit.native.harness === 'codex') {
|
|
42
54
|
if (!config.codex)
|
|
@@ -49,10 +61,10 @@ export async function createNativeServe(root, deps = {}) {
|
|
|
49
61
|
else
|
|
50
62
|
adapter = langchainHarness();
|
|
51
63
|
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 });
|
|
64
|
+
const bridge = await createNativeBridge({ name: config.agent.name, ownerId: config.agent.ownerId, root: paths.state, port: config.port, allowedHosts: publicHosts(config), authenticate, adapter, maxActiveTurns: config.maxActiveTurns ?? 2, log, appCapabilities });
|
|
53
65
|
// Portal marks a runtime expired unless it re-acknowledges its signed config within the 300s lease;
|
|
54
66
|
// an idle serve process has no per-turn MCP child doing that, so it keeps the lease itself.
|
|
55
|
-
|
|
67
|
+
runtime = deps.runtimeSync === false ? undefined : startRuntimeSync(root, { log, ...deps.runtimeSync });
|
|
56
68
|
return { ...bridge, port: config.port, harness: kit.native.harness, runtime, close: async () => { await runtime?.stop(); await bridge.close(); await codex?.close(); } };
|
|
57
69
|
}
|
|
58
70
|
export async function serveNative(root) {
|
package/dist/native.d.ts
CHANGED
|
@@ -37,5 +37,6 @@ export declare function nativeConnector(): {
|
|
|
37
37
|
}>;
|
|
38
38
|
call: (registrationId: string, operation: KnowledgeOperation, input?: Record<string, unknown>, signal?: AbortSignal) => Promise<any>;
|
|
39
39
|
};
|
|
40
|
+
export declare const nativeUsage = "Usage: tealbrick native setup --harness claude|codex|langchain [--workspace-name NAME | --create-workspace NAME] [--agent-name NAME | --create-agent NAME] [--issuer HTTPS_ORIGIN] | native mcp [--root DIRECTORY] | native serve [--root DIRECTORY] [--install-launchd [--no-load] | --uninstall] | native enroll --url HTTPS_ORIGIN [--port PORT] [--root DIRECTORY] [--skip-endpoint-check] | native enable|disable | native marketplace-enable|marketplace-disable";
|
|
40
41
|
export declare function nativeCommand(args: string[], root: string): Promise<void>;
|
|
41
42
|
export { guidedNativeSetup, nativeSdkConfig };
|
package/dist/native.js
CHANGED
|
@@ -64,6 +64,7 @@ function serveFlags(args, values, switches) {
|
|
|
64
64
|
}
|
|
65
65
|
return flags;
|
|
66
66
|
}
|
|
67
|
+
export const nativeUsage = 'Usage: tealbrick native setup --harness claude|codex|langchain [--workspace-name NAME | --create-workspace NAME] [--agent-name NAME | --create-agent NAME] [--issuer HTTPS_ORIGIN] | native mcp [--root DIRECTORY] | native serve [--root DIRECTORY] [--install-launchd [--no-load] | --uninstall] | native enroll --url HTTPS_ORIGIN [--port PORT] [--root DIRECTORY] [--skip-endpoint-check] | native enable|disable | native marketplace-enable|marketplace-disable';
|
|
67
68
|
export async function nativeCommand(args, root) {
|
|
68
69
|
if (args[0] === 'serve' || args[0] === 'enroll') {
|
|
69
70
|
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']);
|
|
@@ -111,6 +112,11 @@ export async function nativeCommand(args, root) {
|
|
|
111
112
|
return;
|
|
112
113
|
}
|
|
113
114
|
const command = args[0], flags = {};
|
|
115
|
+
// A bare `tealbrick native` or help request prints usage; it is not a failed setup.
|
|
116
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
117
|
+
console.log(nativeUsage);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
114
120
|
for (let i = 1; i < args.length; i += 2) {
|
|
115
121
|
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)])
|
|
116
122
|
throw Error('kit_native_invalid_arguments');
|
|
@@ -171,6 +177,7 @@ export async function nativeCommand(args, root) {
|
|
|
171
177
|
console.log(JSON.stringify({ active: command === 'enable' }));
|
|
172
178
|
return;
|
|
173
179
|
}
|
|
174
|
-
|
|
180
|
+
console.error(nativeUsage);
|
|
181
|
+
process.exitCode = 2;
|
|
175
182
|
}
|
|
176
183
|
export { guidedNativeSetup, nativeSdkConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tealbrick/kit",
|
|
3
|
-
"version": "0.3.0-rc.
|
|
3
|
+
"version": "0.3.0-rc.7",
|
|
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.
|
|
47
|
-
"@tealbrick/avm": "0.3.0-rc.
|
|
48
|
-
"@tealbrick/voice": "0.3.0-rc.
|
|
49
|
-
"@tealbrick/vision": "0.3.0-rc.
|
|
50
|
-
"@tealbrick/deliver": "0.3.0-rc.
|
|
46
|
+
"@tealbrick/portal": "0.3.0-rc.7",
|
|
47
|
+
"@tealbrick/avm": "0.3.0-rc.7",
|
|
48
|
+
"@tealbrick/voice": "0.3.0-rc.7",
|
|
49
|
+
"@tealbrick/vision": "0.3.0-rc.7",
|
|
50
|
+
"@tealbrick/deliver": "0.3.0-rc.7",
|
|
51
51
|
"@inquirer/prompts": "8.7.2",
|
|
52
52
|
"zod": "^4.0.0"
|
|
53
53
|
},
|