@tealbrick/kit 0.3.0-rc.1 → 0.3.0-rc.4
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 +9 -3
- package/README.md +115 -2
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.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-selection.js +19 -4
- package/dist/native-serve-config.d.ts +160 -0
- package/dist/native-serve-config.js +190 -0
- package/dist/native-serve.d.ts +50 -0
- package/dist/native-serve.js +124 -0
- package/dist/native-setup.js +38 -15
- package/dist/native.d.ts +2 -0
- package/dist/native.js +75 -3
- package/dist/onboarding.d.ts +5 -0
- package/dist/onboarding.js +1 -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.4 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.4` 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.4
|
|
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.4** 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 adds `tealbrick native serve` and `native enroll` (see README.md).
|
|
4
4
|
|
|
5
5
|
## Install and connect
|
|
6
6
|
|
|
7
|
-
Install
|
|
7
|
+
Install with `npm install --save-exact tealbrick@0.3.0-rc.4` (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
|
|
|
@@ -50,7 +50,7 @@ const adapter = new MCPAdapter(config);
|
|
|
50
50
|
const agent = createAgent({model: configuredModel, tools: await adapter.listTools(), checkpointer});
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Configure provider authentication on the native host through its supported SDK. Kit does not copy login caches, choose providers or change billing. No Eve daemon, AVMM or inbound bridge is required. Native mode emits no Eve mounts and uses the existing kit state and owner-only credential file.
|
|
53
|
+
Configure provider authentication on the native host through its supported SDK. Kit does not copy login caches, choose providers or change billing. No Eve daemon, AVMM or inbound bridge is required. An optional owner-only chat endpoint for Teal Brick Desktop is available through `tealbrick native serve` and `tealbrick native enroll`; see README.md. Native mode emits no Eve mounts and uses the existing kit state and owner-only credential file.
|
|
54
54
|
|
|
55
55
|
## Children and host authority
|
|
56
56
|
|
|
@@ -75,3 +75,9 @@ Each request reloads local activation and fetches signed Portal authorization. R
|
|
|
75
75
|
## Acceptance boundary
|
|
76
76
|
|
|
77
77
|
Disposable installed-artifact tests cover empty-account creation, named selection, permission/ambiguity denial, lost-response retry, signed config, scoped Knowledge-fixture read, revoke/reconnect, deactivation and MCP cancellation. They are not real browser consent or real Knowledge product proof. Root's separate Foxy receipt proves the earlier packed candidate's live Claude MCP inactive denial, exact-session process restart and compaction. Codex and LangChain have independent narrower fixture evidence, not live-provider parity. Marketplace, voice/vision and delegated Tealbrick grants are outside this Stage A claim.
|
|
78
|
+
|
|
79
|
+
## Name conflicts and interrupted setup
|
|
80
|
+
|
|
81
|
+
Existing cards are shown with their harness. A card for Eve cannot be converted to Claude by choosing its name. If a name is already used, select an explicitly compatible card or choose another name; setup keeps the approved login during this interaction. Duplicate compatible names require selecting the exact card ID.
|
|
82
|
+
|
|
83
|
+
A definitive Portal name-conflict rejection permits changing the proposed identity. A timeout or lost response does not: the private journal keeps the original request and idempotency key. Repeat `tealbrick setup --harness <harness>` without new identity-selection flags to reconcile that request. Changing an uncertain request fails with `kit_native_setup_pending_intent_mismatch`; do not delete the journal to bypass it.
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
> Prerelease 0.3.0-rc.
|
|
1
|
+
> Prerelease 0.3.0-rc.4 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.4
|
|
13
13
|
npx --no-install tealbrick setup
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -92,3 +92,116 @@ including a single deduplicated provider-transport installation.
|
|
|
92
92
|
## AVMM profiles
|
|
93
93
|
|
|
94
94
|
The `avm` selection accepts the [AVM package connection/policy schema](../avm/README.md): either `{target,sudo}` or `{selectedProfile,profiles}`. Setup can select a saved profile and preserves its grants. New connections can use paired AVMM users or operator SSH. Paired mode requires a dedicated SSH identity path and a resolvable hostname; no credentials are copied. Use `tealbrick apply config.json` for full workspace, ownership, sharing, pool and ingress grants. Disabling AVM removes its entire mount.
|
|
95
|
+
|
|
96
|
+
## Native chat endpoint: `tealbrick native serve`
|
|
97
|
+
|
|
98
|
+
Optional. Lets the owner chat with a native **Claude Agent SDK** or **Codex**
|
|
99
|
+
agent from TBD. The agent must already be set up with
|
|
100
|
+
`tealbrick setup --harness claude|codex` (kit state, runtime connection and
|
|
101
|
+
`.tealbrick/native-sdk.json`). LangChain is refused with
|
|
102
|
+
`kit_native_serve_langchain_unsupported`.
|
|
103
|
+
|
|
104
|
+
The endpoint is the Eve session subset Desktop uses (`/eve/v1/health`,
|
|
105
|
+
`/eve/v1/session` create/send, NDJSON stream with cursor catch-up, cancel,
|
|
106
|
+
`/eve/v1/session/:id/access`) plus `GET /tealbrick/v1/info`, which reports
|
|
107
|
+
`harness`, `protocol: "tealbrick-native"`, `model`, `capabilities` and
|
|
108
|
+
`unsupported` honestly. `/eve/v1/info`, reset/clear/compact, attachments,
|
|
109
|
+
approvals, subagents, workflows and schedules return 404/400. The native
|
|
110
|
+
harness keeps the conversation: Claude turns resume the same SDK session id,
|
|
111
|
+
and a restarted server only re-attaches transcripts that still exist.
|
|
112
|
+
|
|
113
|
+
### Security posture
|
|
114
|
+
|
|
115
|
+
- Binds `127.0.0.1` only. Reach it from the tailnet through a TLS-terminating
|
|
116
|
+
proxy (preferred: `tailscale serve`, including for customer deployments).
|
|
117
|
+
- Every request except `/healthz` is verified against Portal (identity token
|
|
118
|
+
plus policy bundle for this agent) and must come from the enrolled owner.
|
|
119
|
+
- The `Host` header must be loopback, the enrolled `publicUrl` host or an
|
|
120
|
+
explicit `allowedHosts` entry. Any `Origin` header is rejected. Bodies are
|
|
121
|
+
limited to 128 KiB and two concurrent turns by default (`maxActiveTurns`).
|
|
122
|
+
- New sessions are **Restricted**: `Read`/`Glob`/`Grep` contained to the
|
|
123
|
+
profile `cwd` (never the kit's `.tealbrick`), operator-vetted `readTools`, and
|
|
124
|
+
`mcp__tealbrick__*` only. Shell, file writes, network tools, delegation and
|
|
125
|
+
project/user Claude settings are unavailable; a PreToolUse hook and
|
|
126
|
+
`canUseTool` enforce the same decision. The kit's child-agent guard still
|
|
127
|
+
denies Teal Brick tools to subagents. Optional `tealbrickCalls` adds a
|
|
128
|
+
local operation ceiling on top of Portal grants.
|
|
129
|
+
- A per-agent default can widen Restricted with `defaultTools` (only
|
|
130
|
+
`WebSearch`, `WebFetch`, `Agent`, `TodoWrite`; never shell or writes).
|
|
131
|
+
`Agent` delegates only to the configured `experts`, in the foreground with
|
|
132
|
+
the session model; each expert runs with its own `tools` (workspace reads,
|
|
133
|
+
`WebSearch`, `WebFetch`) and never receives Teal Brick tools.
|
|
134
|
+
- Elevation only through Desktop's session-access control (owner, explicit
|
|
135
|
+
confirmation, optimistic revision, no turn in flight). **Native defaults**
|
|
136
|
+
snapshots `nativeTools` (still workspace-contained); **Full** uses the
|
|
137
|
+
Claude Code tool preset with permission bypass. Full is not a security
|
|
138
|
+
boundary against the local OS user.
|
|
139
|
+
- The SDK process receives an allowlisted environment (`PATH`, `HOME`, locale,
|
|
140
|
+
CA settings, `envPassthrough`) and never `TEALBRICK_*` values. Logs carry ids,
|
|
141
|
+
tool names and status codes, never prompts, tool inputs or results.
|
|
142
|
+
|
|
143
|
+
### Configure
|
|
144
|
+
|
|
145
|
+
`.tealbrick/native-serve.json` (owner-only, `chmod 600`) holds no secrets:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"version": 1,
|
|
150
|
+
"port": 5331,
|
|
151
|
+
"claude": {
|
|
152
|
+
"cwd": "/Users/you/agents/henry-serve/workspace",
|
|
153
|
+
"instructionsFile": "/Users/you/agents/henry-calendar/agents/henry/INSTRUCTIONS.md",
|
|
154
|
+
"recipe": {"path": "/Users/you/.config/tealbrick-native/config.json", "agent": "henry"},
|
|
155
|
+
"requireSubscription": true,
|
|
156
|
+
"defaultTools": ["WebSearch", "WebFetch", "Agent"],
|
|
157
|
+
"experts": {
|
|
158
|
+
"source-researcher": {"promptFile": "/Users/you/agents/henry-calendar/experts/source-researcher.md", "tools": ["WebSearch", "WebFetch"]},
|
|
159
|
+
"independent-reviewer": {"promptFile": "/Users/you/agents/henry-calendar/experts/independent-reviewer.md"}
|
|
160
|
+
},
|
|
161
|
+
"nativeTools": ["Read", "Glob", "Grep", "Write", "Edit", "TodoWrite", "WebSearch", "WebFetch"]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`recipe` imports `model`, `effort`, `maxTurns`, `maxBudgetUsd`,
|
|
167
|
+
`timeoutSeconds` and the agent's `mcpServers`, `readTools` and
|
|
168
|
+
`tealbrickCalls` from an existing tealbrick-native recipe; its
|
|
169
|
+
`tealbrickSdkConfig` must point at this kit root's `native-sdk.json`. Any of
|
|
170
|
+
those keys set directly under `claude` override the recipe. Keep `cwd` stable:
|
|
171
|
+
Claude session transcripts are stored per working directory. `requireSubscription`
|
|
172
|
+
refuses turns unless the SDK reports first-party subscription sign-in.
|
|
173
|
+
Codex roots use `"codex": {"codexBin": "/abs/codex", "cwd": "/abs/dir"}`.
|
|
174
|
+
|
|
175
|
+
Install the Claude Agent SDK next to the kit (or set `claude.sdkModule` to an
|
|
176
|
+
absolute SDK package directory):
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
npm install --save-exact @anthropic-ai/claude-agent-sdk@0.3.287
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Run, expose, enroll
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
# 1. Tailnet HTTPS in front of the loopback port (pick a free HTTPS port).
|
|
186
|
+
tailscale serve --bg --https=8444 http://127.0.0.1:5331
|
|
187
|
+
# 2. Record the Portal identity and register the URL on the EXISTING agent.
|
|
188
|
+
npx --no-install tealbrick native enroll --url https://HOST.TAILNET.ts.net:8444
|
|
189
|
+
# 3. Start the endpoint (foreground) ...
|
|
190
|
+
npx --no-install tealbrick native serve
|
|
191
|
+
# ... or supervise it with a user LaunchAgent (macOS, opt-in).
|
|
192
|
+
npx --no-install tealbrick native serve --install-launchd
|
|
193
|
+
npx --no-install tealbrick native serve --uninstall
|
|
194
|
+
# 4. Re-run enroll to confirm /healthz and authenticated /tealbrick/v1/info.
|
|
195
|
+
npx --no-install tealbrick native enroll --url https://HOST.TAILNET.ts.net:8444
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`enroll` signs in with Portal device approval, requires the kit root's owned
|
|
199
|
+
agent and canvas card to carry the native harness label, registers the origin
|
|
200
|
+
through Portal's owned-connection API (which keeps the harness label; it never
|
|
201
|
+
uses the Eve registration route, which relabels agents as Eve), records
|
|
202
|
+
`agent` and `publicUrl` in `native-serve.json`, updates the card endpoint with
|
|
203
|
+
a revision-checked draft write and verifies the sidebar URL. It then probes the
|
|
204
|
+
endpoint; exit code 2 means Portal is updated but the endpoint was not yet
|
|
205
|
+
reachable or authenticated. The user token is never written to disk.
|
|
206
|
+
LaunchAgent logs go to `.tealbrick/native-serve/logs/`; session projections
|
|
207
|
+
(customer data) to `.tealbrick/native-serve/sessions/`.
|
package/dist/cli.js
CHANGED
|
@@ -127,4 +127,4 @@ async function main() {
|
|
|
127
127
|
console.log('Portal agent card saved and confirmed in workspace discovery. Rebuild/restart Eve to connect.');
|
|
128
128
|
console.log('Local credentials stay in .tealbrick/kit.credentials.json (owner-only). Supply them as runtime secrets when deploying. Provider connectivity has not been tested.');
|
|
129
129
|
}
|
|
130
|
-
main().catch(error => { const message = error instanceof Error ? error.message : ''; console.error(/^kit_[a-z_]+(?::agent\/[a-zA-Z0-9_./-]+)
|
|
130
|
+
main().catch(error => { const message = error instanceof Error ? error.message : ''; console.error(/^(?:kit_[a-z_]+(?::agent\/[a-zA-Z0-9_./-]+|:[a-zA-Z0-9_.,]+)?|runtime_product_inactive)$/.test(message) ? message : 'Kit setup failed; check configuration and permissions. Credentials omitted.'); process.exitCode = 1; });
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const ref = z.string().regex(/^TEALBRICK_[A-Z0-9_]+$/);
|
|
|
12
12
|
const runtime = z.object({ issuer: z.string().url(), org: z.string().min(1), workspaceId: z.string().min(1), agentId: z.string().min(1), connectionId: z.string().min(1), credentialRef: ref, bindings: z.record(z.string(), z.object({ endpoint: z.string().url(), instanceId: z.string().min(1), companyId: z.string().min(1), credentialRef: ref }).strict()) }).strict();
|
|
13
13
|
const command = z.tuple([z.string().min(1)]).rest(z.string());
|
|
14
14
|
const routines = z.object({ url: z.string().url(), credentialRef: ref, scope: z.object({ org: z.string().min(1), workspace: z.string().min(1), agent: z.string().min(1), owner: z.string().min(1) }).strict(), buildCommand: command, restartCommand: command }).strict();
|
|
15
|
-
const schema = z.object({ version: z.literal(1), native: z.object({ harness: z.enum(['claude', 'codex', 'langchain']), active: z.boolean() }).strict().optional(), selfEvolution: z.object({ policyModule: z.string().regex(/^deployment\/[a-zA-Z0-9_-]+\.(ts|js|mjs)$/) }).strict().optional(), routines: routines.optional(), portal: auth.optional(), runtime: runtime.optional(), voice: media.optional(), vision: media.optional(), deliver: z.unknown().optional(), avm: z.custom(v => avmConfigSchema.safeParse(v).success).transform(v => 'target' in v ? { target: v.target, sudo: v.sudo ?? false, ...Object.fromEntries(Object.entries(v).filter(([k]) => k !== 'target' && k !== 'sudo')) } : v).optional() }).strict();
|
|
15
|
+
const schema = z.object({ version: z.literal(1), native: z.object({ harness: z.enum(['claude', 'codex', 'langchain']), active: z.boolean(), marketplace: z.boolean().optional() }).strict().optional(), selfEvolution: z.object({ policyModule: z.string().regex(/^deployment\/[a-zA-Z0-9_-]+\.(ts|js|mjs)$/) }).strict().optional(), routines: routines.optional(), portal: auth.optional(), runtime: runtime.optional(), voice: media.optional(), vision: media.optional(), deliver: z.unknown().optional(), avm: z.custom(v => avmConfigSchema.safeParse(v).success).transform(v => 'target' in v ? { target: v.target, sudo: v.sudo ?? false, ...Object.fromEntries(Object.entries(v).filter(([k]) => k !== 'target' && k !== 'sudo')) } : v).optional() }).strict();
|
|
16
16
|
export const capabilities = ['portal', 'runtime', 'voice', 'vision', 'deliver', 'avm', 'routines', 'selfEvolution'];
|
|
17
17
|
const marker = '// Managed by @tealbrick/kit. Change selection with tealbrick setup.\n';
|
|
18
18
|
const render = (body) => marker + body + '\n';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AccessMode, HarnessAdapter } from '@tealbrick/portal/native-bridge';
|
|
2
|
+
import type { ClaudeAttachment, ClaudeProfile } from './native-serve-config.js';
|
|
3
|
+
/** Minimal structural view of @anthropic-ai/claude-agent-sdk (validated against 0.3.287). */
|
|
4
|
+
export interface ClaudeQuery extends AsyncIterable<any> {
|
|
5
|
+
interrupt?(): Promise<unknown>;
|
|
6
|
+
close?(): void;
|
|
7
|
+
accountInfo?(): Promise<any>;
|
|
8
|
+
}
|
|
9
|
+
export interface ClaudeSdk {
|
|
10
|
+
query(params: {
|
|
11
|
+
prompt: AsyncIterable<any>;
|
|
12
|
+
options: Record<string, any>;
|
|
13
|
+
}): ClaudeQuery;
|
|
14
|
+
getSessionInfo?(sessionId: string, options?: {
|
|
15
|
+
dir?: string;
|
|
16
|
+
}): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
export declare function loadClaudeSdk(root: string, sdkModule?: string): Promise<ClaudeSdk>;
|
|
19
|
+
export type ClaudeAccess = {
|
|
20
|
+
mode: AccessMode;
|
|
21
|
+
revision: number;
|
|
22
|
+
tools: string[];
|
|
23
|
+
};
|
|
24
|
+
/** Built-in tools Restricted mode exposes: read-only and contained to the workspace. */
|
|
25
|
+
export declare const readOnlyTools: string[];
|
|
26
|
+
/** Removed from the model's context unless a wider access mode explicitly lists them. */
|
|
27
|
+
export declare const restrictedDenied: string[];
|
|
28
|
+
export interface ToolPolicy {
|
|
29
|
+
access: ClaudeAccess;
|
|
30
|
+
profile: ClaudeProfile;
|
|
31
|
+
protectedPaths: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Single decision point for PreToolUse hooks and canUseTool. Names and decisions only are logged. */
|
|
34
|
+
export declare function decideTool(policy: ToolPolicy, name: string, input: Record<string, unknown>, agentId?: string, agentType?: string): Promise<{
|
|
35
|
+
allow: boolean;
|
|
36
|
+
reason: string;
|
|
37
|
+
}>;
|
|
38
|
+
export interface ClaudeHarnessOptions {
|
|
39
|
+
name: string;
|
|
40
|
+
profile: ClaudeProfile;
|
|
41
|
+
attachment: ClaudeAttachment;
|
|
42
|
+
sdk: ClaudeSdk;
|
|
43
|
+
protectedPaths: string[];
|
|
44
|
+
log?: (event: Record<string, unknown>) => void;
|
|
45
|
+
env?: NodeJS.ProcessEnv;
|
|
46
|
+
}
|
|
47
|
+
/** Claude Agent SDK adapter. Claude's persisted session transcript is the source of truth;
|
|
48
|
+
* every turn is a fresh query() that resumes the same session id. */
|
|
49
|
+
export declare function claudeHarness(options: ClaudeHarnessOptions): HarnessAdapter<ClaudeAccess>;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { realpath } from 'node:fs/promises';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, basename } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
export async function loadClaudeSdk(root, sdkModule) {
|
|
7
|
+
let entry;
|
|
8
|
+
try {
|
|
9
|
+
entry = sdkModule ? createRequire(import.meta.url).resolve(sdkModule) : createRequire(join(root, 'package.json')).resolve('@anthropic-ai/claude-agent-sdk');
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw Error('kit_native_claude_sdk_missing');
|
|
13
|
+
}
|
|
14
|
+
const sdk = await import(pathToFileURL(entry).href);
|
|
15
|
+
if (typeof sdk.query !== 'function')
|
|
16
|
+
throw Error('kit_native_claude_sdk_invalid');
|
|
17
|
+
return sdk;
|
|
18
|
+
}
|
|
19
|
+
/** Built-in tools Restricted mode exposes: read-only and contained to the workspace. */
|
|
20
|
+
export const readOnlyTools = ['Read', 'Glob', 'Grep'];
|
|
21
|
+
/** Removed from the model's context unless a wider access mode explicitly lists them. */
|
|
22
|
+
export const restrictedDenied = ['Bash', 'BashOutput', 'KillShell', 'KillBash', 'Monitor', 'Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'WebFetch', 'WebSearch', 'Agent', 'Task', 'TodoWrite', 'Skill', 'SlashCommand', 'ExitPlanMode', 'EnterWorktree', 'ExitWorktree'];
|
|
23
|
+
const pathTools = { Read: ['file_path'], Write: ['file_path'], Edit: ['file_path'], MultiEdit: ['file_path'], NotebookEdit: ['notebook_path'], Glob: ['path'], Grep: ['path'] };
|
|
24
|
+
const safeEnvNames = ['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'TMPDIR', 'LANG', 'LC_ALL', 'SSL_CERT_FILE', 'NODE_EXTRA_CA_CERTS'];
|
|
25
|
+
async function contained(root, input, deny) {
|
|
26
|
+
let path = resolve(root, input), tail = [];
|
|
27
|
+
for (;;) {
|
|
28
|
+
try {
|
|
29
|
+
path = resolve(await realpath(path), ...tail);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
if (e.code !== 'ENOENT')
|
|
34
|
+
return false;
|
|
35
|
+
const parent = dirname(path);
|
|
36
|
+
if (parent === path)
|
|
37
|
+
return false;
|
|
38
|
+
tail.unshift(basename(path));
|
|
39
|
+
path = parent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const inside = (base) => { const rel = relative(base, path); return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); };
|
|
43
|
+
return inside(await realpath(root)) && !deny.some(inside);
|
|
44
|
+
}
|
|
45
|
+
/** Single decision point for PreToolUse hooks and canUseTool. Names and decisions only are logged. */
|
|
46
|
+
export async function decideTool(policy, name, input, agentId, agentType) {
|
|
47
|
+
const { access, profile } = policy;
|
|
48
|
+
if (name.startsWith('mcp__tealbrick__')) {
|
|
49
|
+
if (agentId !== undefined)
|
|
50
|
+
return { allow: false, reason: 'Teal Brick authority is not delegated to child agents' };
|
|
51
|
+
if (name === 'mcp__tealbrick__tealbrick_call' && profile.tealbrickCalls.length) {
|
|
52
|
+
const args = input.input;
|
|
53
|
+
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));
|
|
54
|
+
if (!ok)
|
|
55
|
+
return { allow: false, reason: 'Outside the configured Teal Brick operation ceiling' };
|
|
56
|
+
}
|
|
57
|
+
return { allow: true, reason: 'Portal-governed Teal Brick tool' };
|
|
58
|
+
}
|
|
59
|
+
if (access.mode === 'full')
|
|
60
|
+
return { allow: true, reason: 'Full session access' };
|
|
61
|
+
// The SDK still surfaces the delegation tool under its legacy name Task.
|
|
62
|
+
if (name === 'Task')
|
|
63
|
+
name = 'Agent';
|
|
64
|
+
const allowed = access.mode === 'restricted' ? [...[...readOnlyTools, ...profile.defaultTools].filter(t => access.tools.includes(t)), ...profile.readTools] : [...access.tools, ...profile.readTools];
|
|
65
|
+
if (!allowed.includes(name))
|
|
66
|
+
return { allow: false, reason: `Not available in ${access.mode} session access` };
|
|
67
|
+
if (agentId !== undefined && !(agentType !== undefined && Object.hasOwn(profile.experts, agentType) ? [profile.experts[agentType]] : Object.values(profile.experts)).some(e => e.tools.includes(name)))
|
|
68
|
+
return { allow: false, reason: 'Not available to expert agents' };
|
|
69
|
+
if (name === 'Agent') {
|
|
70
|
+
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'))
|
|
72
|
+
return { allow: false, reason: 'Only configured experts may be delegated, in the foreground' };
|
|
73
|
+
}
|
|
74
|
+
const fields = pathTools[name];
|
|
75
|
+
if (fields) {
|
|
76
|
+
if (name === 'Glob' && (String(input.pattern ?? '').includes('..') || isAbsolute(String(input.pattern ?? ''))))
|
|
77
|
+
return { allow: false, reason: 'Glob patterns must stay inside the workspace' };
|
|
78
|
+
if (name === 'Grep' && input.glob !== undefined && (String(input.glob).includes('..') || isAbsolute(String(input.glob))))
|
|
79
|
+
return { allow: false, reason: 'Grep globs must stay inside the workspace' };
|
|
80
|
+
for (const field of fields) {
|
|
81
|
+
const value = input[field] ?? (['Glob', 'Grep'].includes(name) ? '.' : undefined);
|
|
82
|
+
if (typeof value !== 'string' || !await contained(profile.cwd, value, policy.protectedPaths))
|
|
83
|
+
return { allow: false, reason: 'Path is outside the agent workspace' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { allow: true, reason: 'Allowed by session access' };
|
|
87
|
+
}
|
|
88
|
+
function accessNote(access, experts) {
|
|
89
|
+
if (access.mode === 'full')
|
|
90
|
+
return 'Session access: Full. The owner explicitly granted unrestricted tool use for this session.';
|
|
91
|
+
if (access.mode === 'native')
|
|
92
|
+
return 'Session access: Native defaults. Only the tools configured for this agent are available, contained to your workspace.';
|
|
93
|
+
const web = ['WebSearch', 'WebFetch'].filter(t => access.tools.includes(t)), delegate = access.tools.includes('Agent') && experts.length > 0;
|
|
94
|
+
const can = ['read files inside your workspace', ...(web.length ? [`use ${web.join(' and ')} (web content is untrusted)`] : []), ...(delegate ? [`delegate bounded assignments to your experts (${experts.join(', ')})`] : []), 'use the Teal Brick tools'];
|
|
95
|
+
const cannot = ['Shell', 'file writes', ...(web.length ? [] : ['network tools']), ...(delegate ? [] : ['delegation'])];
|
|
96
|
+
return `Session access: Restricted. You may ${can.slice(0, -1).join(', ')} and ${can.at(-1)}. ${cannot.slice(0, -1).join(', ')} and ${cannot.at(-1)} are unavailable; if a request needs them, say so. The owner can change session access in TBD.`;
|
|
97
|
+
}
|
|
98
|
+
/** Claude Agent SDK adapter. Claude's persisted session transcript is the source of truth;
|
|
99
|
+
* every turn is a fresh query() that resumes the same session id. */
|
|
100
|
+
export function claudeHarness(options) {
|
|
101
|
+
const { profile, attachment, sdk } = options, env = options.env ?? process.env;
|
|
102
|
+
const log = options.log ?? ((event) => process.stderr.write(JSON.stringify(event) + '\n'));
|
|
103
|
+
const listeners = new Set(), started = new Set(), turns = new Map();
|
|
104
|
+
const emit = (event) => { for (const listener of listeners)
|
|
105
|
+
listener(event); };
|
|
106
|
+
const restricted = () => ({ mode: 'restricted', revision: 0, tools: [...readOnlyTools, ...profile.defaultTools] });
|
|
107
|
+
const experts = Object.keys(profile.experts);
|
|
108
|
+
const agents = Object.fromEntries(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 }]));
|
|
109
|
+
const childEnv = () => {
|
|
110
|
+
const out = { CLAUDE_AGENT_SDK_CLIENT_APP: 'tealbrick-kit/native-serve' };
|
|
111
|
+
for (const name of [...safeEnvNames, ...profile.envPassthrough])
|
|
112
|
+
if (env[name] && !name.startsWith('TEALBRICK_'))
|
|
113
|
+
out[name] = env[name];
|
|
114
|
+
return out;
|
|
115
|
+
};
|
|
116
|
+
const optionsFor = (sessionId, access, abort) => {
|
|
117
|
+
const policy = { access, profile, protectedPaths: options.protectedPaths };
|
|
118
|
+
const builtins = access.tools.filter(t => !t.startsWith('mcp__'));
|
|
119
|
+
const servers = access.mode === 'restricted' ? Object.fromEntries(Object.entries(profile.mcpServers).filter(([name]) => profile.readTools.some(t => t.startsWith(`mcp__${name}__`)))) : profile.mcpServers;
|
|
120
|
+
const hook = async (input) => {
|
|
121
|
+
if (input?.hook_event_name !== 'PreToolUse')
|
|
122
|
+
return {};
|
|
123
|
+
const decision = await decideTool(policy, String(input.tool_name), input.tool_input ?? {}, input.agent_id, input.agent_type);
|
|
124
|
+
log({ event: 'claude.tool', sessionId, tool: String(input.tool_name).slice(0, 120), allowed: decision.allow, child: input.agent_id !== undefined });
|
|
125
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: decision.allow ? 'allow' : 'deny', permissionDecisionReason: decision.reason } };
|
|
126
|
+
};
|
|
127
|
+
const append = [profile.instructions?.trim(), `You are ${options.name}, chatting with your owner through TBD. ${accessNote(access, experts)}`].filter(Boolean).join('\n\n');
|
|
128
|
+
const delegates = experts.length > 0 && (access.mode === 'full' || builtins.includes('Agent'));
|
|
129
|
+
return {
|
|
130
|
+
cwd: profile.cwd, env: childEnv(),
|
|
131
|
+
...(profile.model ? { model: profile.model } : {}), ...(profile.effort ? { effort: profile.effort } : {}),
|
|
132
|
+
maxTurns: profile.maxTurns, ...(profile.maxBudgetUsd !== undefined ? { maxBudgetUsd: profile.maxBudgetUsd } : {}),
|
|
133
|
+
settingSources: [], strictMcpConfig: true,
|
|
134
|
+
mcpServers: { ...servers, tealbrick: attachment.mcpServers.tealbrick },
|
|
135
|
+
settings: attachment.settings,
|
|
136
|
+
tools: access.mode === 'full' ? { type: 'preset', preset: 'claude_code' } : builtins,
|
|
137
|
+
...(delegates ? { agents } : {}),
|
|
138
|
+
disallowedTools: access.mode === 'full' ? [] : restrictedDenied.filter(t => !builtins.includes(t === 'Task' ? 'Agent' : t)),
|
|
139
|
+
permissionMode: access.mode === 'full' ? 'bypassPermissions' : 'default',
|
|
140
|
+
...(access.mode === 'full' ? { allowDangerouslySkipPermissions: true } : {
|
|
141
|
+
canUseTool: async (name, input) => { const d = await decideTool(policy, name, input); return d.allow ? { behavior: 'allow', updatedInput: input } : { behavior: 'deny', message: d.reason }; },
|
|
142
|
+
}),
|
|
143
|
+
hooks: { PreToolUse: [{ hooks: [hook] }] },
|
|
144
|
+
systemPrompt: { type: 'preset', preset: 'claude_code', append },
|
|
145
|
+
persistSession: true, includePartialMessages: true,
|
|
146
|
+
...(started.has(sessionId) ? { resume: sessionId } : { sessionId }),
|
|
147
|
+
abortController: abort,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
async function run(sessionId, text, access) {
|
|
151
|
+
const turn = { cancelled: false, timedOut: false, turnId: randomUUID() }, abort = new AbortController();
|
|
152
|
+
turn.abort = abort;
|
|
153
|
+
turns.set(sessionId, turn);
|
|
154
|
+
emit({ type: 'turn.started', sessionId, turnId: turn.turnId });
|
|
155
|
+
let release;
|
|
156
|
+
const gate = new Promise(r => { release = r; });
|
|
157
|
+
async function* prompt() { await gate; if (abort.signal.aborted)
|
|
158
|
+
return; yield { type: 'user', session_id: '', parent_tool_use_id: null, message: { role: 'user', content: text } }; }
|
|
159
|
+
const timer = setTimeout(() => { turn.timedOut = true; abort.abort(); }, profile.timeoutSeconds * 1000);
|
|
160
|
+
let result, assistantError, failure, finalText = false, buffer = '', stopReason = null;
|
|
161
|
+
try {
|
|
162
|
+
const q = sdk.query({ prompt: prompt(), options: optionsFor(sessionId, access, abort) });
|
|
163
|
+
turn.query = q;
|
|
164
|
+
if (profile.requireSubscription) {
|
|
165
|
+
const account = await q.accountInfo?.();
|
|
166
|
+
if (!account || account.apiProvider !== 'firstParty' || !account.subscriptionType || account.apiKeySource === 'env')
|
|
167
|
+
failure = { code: 'claude_subscription_required', message: 'Claude subscription sign-in is required on this host; API-key fallback is disabled.' };
|
|
168
|
+
}
|
|
169
|
+
if (failure)
|
|
170
|
+
abort.abort();
|
|
171
|
+
else
|
|
172
|
+
release();
|
|
173
|
+
if (!failure)
|
|
174
|
+
for await (const m of q) {
|
|
175
|
+
if (m?.type === 'system' && m.subtype === 'init') {
|
|
176
|
+
if (m.session_id !== sessionId) {
|
|
177
|
+
failure = { code: 'claude_session_mismatch', message: 'The Claude session id did not match this conversation.' };
|
|
178
|
+
abort.abort();
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
started.add(sessionId);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (m?.parent_tool_use_id !== null && m?.parent_tool_use_id !== undefined)
|
|
185
|
+
continue;
|
|
186
|
+
if (m?.type === 'stream_event') {
|
|
187
|
+
const e = m.event ?? {};
|
|
188
|
+
if (e.type === 'message_start') {
|
|
189
|
+
buffer = '';
|
|
190
|
+
stopReason = null;
|
|
191
|
+
}
|
|
192
|
+
else if (e.type === 'content_block_start' && e.content_block?.type === 'tool_use')
|
|
193
|
+
emit({ type: 'action.requested', sessionId, toolName: String(e.content_block.name).slice(0, 120) });
|
|
194
|
+
else if (e.type === 'content_block_delta' && e.delta?.type === 'text_delta' && typeof e.delta.text === 'string') {
|
|
195
|
+
buffer += e.delta.text;
|
|
196
|
+
emit({ type: 'message.delta', sessionId, text: e.delta.text });
|
|
197
|
+
}
|
|
198
|
+
else if (e.type === 'message_delta')
|
|
199
|
+
stopReason = e.delta?.stop_reason ?? stopReason;
|
|
200
|
+
else if (e.type === 'message_stop' && buffer.trim()) {
|
|
201
|
+
const final = stopReason !== 'tool_use';
|
|
202
|
+
emit({ type: 'message.completed', sessionId, text: buffer, finishReason: final ? 'stop' : 'tool-calls' });
|
|
203
|
+
finalText ||= final;
|
|
204
|
+
buffer = '';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (m?.type === 'user' && Array.isArray(m.message?.content) && m.message.content.some((b) => b?.type === 'tool_result'))
|
|
208
|
+
emit({ type: 'action.result', sessionId });
|
|
209
|
+
else if (m?.type === 'assistant' && typeof m.error === 'string')
|
|
210
|
+
assistantError = m.error;
|
|
211
|
+
else if (m?.type === 'result')
|
|
212
|
+
result = m;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch { /* classified below; SDK error text is not echoed */ }
|
|
216
|
+
finally {
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
release();
|
|
219
|
+
try {
|
|
220
|
+
turn.query?.close?.();
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
223
|
+
turns.delete(sessionId);
|
|
224
|
+
}
|
|
225
|
+
if (turn.cancelled) {
|
|
226
|
+
emit({ type: 'turn.ended', sessionId, status: 'cancelled', turnId: turn.turnId });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!failure && turn.timedOut)
|
|
230
|
+
failure = { code: 'claude_turn_timeout', message: `The turn exceeded ${profile.timeoutSeconds} seconds and was stopped.` };
|
|
231
|
+
if (!failure && result?.subtype === 'success' && !result.is_error) {
|
|
232
|
+
if (!finalText && typeof result.result === 'string' && result.result.trim())
|
|
233
|
+
emit({ type: 'message.completed', sessionId, text: result.result, finishReason: 'stop' });
|
|
234
|
+
emit({ type: 'turn.ended', sessionId, status: 'completed', turnId: turn.turnId });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!failure) {
|
|
238
|
+
const subtype = typeof result?.subtype === 'string' && /^[a-z_]+$/.test(result.subtype) ? result.subtype : undefined;
|
|
239
|
+
const messages = { error_max_turns: `The turn reached its ${profile.maxTurns}-turn limit.`, error_max_budget_usd: 'The turn reached its configured budget limit.' };
|
|
240
|
+
failure = { code: 'claude_' + (subtype && subtype !== 'success' ? subtype : assistantError && /^[a-z_]+$/.test(assistantError) ? assistantError : 'turn_failed'), message: (subtype && messages[subtype]) ?? 'The Claude Agent SDK turn failed; check the host sign-in and logs.' };
|
|
241
|
+
}
|
|
242
|
+
log({ event: 'claude.turn.failed', sessionId, code: failure.code });
|
|
243
|
+
emit({ type: 'turn.ended', sessionId, status: 'failed', code: failure.code, message: failure.message, turnId: turn.turnId });
|
|
244
|
+
}
|
|
245
|
+
const capabilities = ['chat', 'resume', 'cancel', 'mcp', 'session-access'];
|
|
246
|
+
return {
|
|
247
|
+
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', 'approvals', 'subagents', 'session-reset', 'session-clear', 'session-compact', 'client-context', 'output-schema'] },
|
|
249
|
+
restricted,
|
|
250
|
+
subscribe: listener => { listeners.add(listener); },
|
|
251
|
+
async create() { return randomUUID(); },
|
|
252
|
+
async resume(sessionId) {
|
|
253
|
+
// Only re-attach a transcript that actually exists; never mint a replacement.
|
|
254
|
+
if (sdk.getSessionInfo && !await sdk.getSessionInfo(sessionId, { dir: profile.cwd }))
|
|
255
|
+
throw Error('claude_session_not_found');
|
|
256
|
+
started.add(sessionId);
|
|
257
|
+
},
|
|
258
|
+
async send(sessionId, message, access) { void run(sessionId, message, access).catch(() => emit({ type: 'turn.ended', sessionId, status: 'failed', code: 'claude_turn_failed', message: 'The Claude Agent SDK turn failed.' })); },
|
|
259
|
+
async cancel(sessionId) {
|
|
260
|
+
const turn = turns.get(sessionId);
|
|
261
|
+
if (!turn)
|
|
262
|
+
return false;
|
|
263
|
+
turn.cancelled = true;
|
|
264
|
+
const interrupted = await Promise.race([Promise.resolve(turn.query?.interrupt?.()).then(() => true, () => false), new Promise(r => setTimeout(() => r(false), 5000))]);
|
|
265
|
+
if (!interrupted)
|
|
266
|
+
turn.abort?.abort();
|
|
267
|
+
else
|
|
268
|
+
setTimeout(() => { if (turns.get(sessionId) === turn)
|
|
269
|
+
turn.abort?.abort(); }, 10000).unref();
|
|
270
|
+
return true;
|
|
271
|
+
},
|
|
272
|
+
access: {
|
|
273
|
+
async select(mode, revision) {
|
|
274
|
+
if (mode === 'restricted')
|
|
275
|
+
return { ...restricted(), revision };
|
|
276
|
+
if (mode === 'full')
|
|
277
|
+
return { mode, revision, tools: ['*'] };
|
|
278
|
+
// Snapshot now: later profile edits never silently widen an existing session.
|
|
279
|
+
return { mode, revision, tools: [...profile.nativeTools] };
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
async close() { for (const turn of turns.values()) {
|
|
283
|
+
turn.cancelled = true;
|
|
284
|
+
turn.abort?.abort();
|
|
285
|
+
} },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PortalLogin } from './onboarding.js';
|
|
2
|
+
/** Register this kit root's public chat endpoint on its EXISTING native Portal agent.
|
|
3
|
+
* Never creates or relabels an identity: the URL goes through Portal's owned-connection
|
|
4
|
+
* upsert, which preserves the agent's harness runtime (the Eve registration route would
|
|
5
|
+
* rewrite it to "eve"). The user token stays in memory. */
|
|
6
|
+
export declare function enrollNative(root: string, input: {
|
|
7
|
+
url: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
}, login: PortalLogin, options?: {
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
verifyEndpoint?: boolean;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
endpointReason?: string | undefined;
|
|
14
|
+
enrolled: boolean;
|
|
15
|
+
agentId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
harness: "Claude Agent SDK" | "Codex" | "LangChain";
|
|
18
|
+
endpoint: string;
|
|
19
|
+
workspaceId: string;
|
|
20
|
+
portalVerified: boolean;
|
|
21
|
+
endpointVerified: boolean;
|
|
22
|
+
}>;
|