@tealbrick/kit 0.2.7 → 0.3.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BOOTSTRAP.md +17 -18
- package/BUZZ.md +146 -0
- package/NATIVE.md +83 -0
- package/README.md +265 -4
- package/RUNTIME.md +1 -1
- package/assets/buzz/buzz-backend-provider.py +99 -0
- package/assets/buzz/buzz-plist.py +22 -0
- package/assets/buzz/buzz-run.sh +39 -0
- package/assets/buzz/tb-provider-apply.py +97 -0
- package/dist/buzz.d.ts +63 -0
- package/dist/buzz.js +262 -0
- package/dist/cli.js +22 -4
- package/dist/index.d.ts +23 -14
- package/dist/index.js +4 -2
- package/dist/native-acp.d.ts +40 -0
- package/dist/native-acp.js +224 -0
- package/dist/native-app-capabilities.d.ts +33 -0
- package/dist/native-app-capabilities.js +37 -0
- package/dist/native-buzz.d.ts +161 -0
- package/dist/native-buzz.js +649 -0
- package/dist/native-child-guard.d.ts +1 -0
- package/dist/native-child-guard.js +22 -0
- package/dist/native-claude.d.ts +99 -0
- package/dist/native-claude.js +584 -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-selection.d.ts +27 -0
- package/dist/native-selection.js +87 -0
- package/dist/native-serve-config.d.ts +286 -0
- package/dist/native-serve-config.js +296 -0
- package/dist/native-serve.d.ts +76 -0
- package/dist/native-serve.js +145 -0
- package/dist/native-setup.d.ts +63 -0
- package/dist/native-setup.js +122 -0
- package/dist/native-standing-grants.d.ts +105 -0
- package/dist/native-standing-grants.js +226 -0
- package/dist/native.d.ts +42 -0
- package/dist/native.js +193 -0
- package/dist/onboarding.d.ts +9 -0
- package/dist/onboarding.js +9 -2
- package/dist/runtime-onboarding.js +3 -3
- package/dist/secrets.d.ts +2 -1
- package/dist/secrets.js +4 -2
- package/package.json +19 -10
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { mkdir, lstat, readFile, writeFile, rename, unlink, realpath } from 'node:fs/promises';
|
|
5
|
+
import { PortalHttpError } from '@tealbrick/portal';
|
|
6
|
+
import { readKit } from './index.js';
|
|
7
|
+
import { selectNativeIdentity, harnessLabels } from './native-selection.js';
|
|
8
|
+
import { setupNative } from './native.js';
|
|
9
|
+
export function nativeSdkConfig(harness, root, options = {}) {
|
|
10
|
+
if (harness === 'codex' && !options.codexDelegationDisabled)
|
|
11
|
+
throw Error('kit_native_codex_delegation_opt_in_required');
|
|
12
|
+
const server = { command: process.execPath, args: [fileURLToPath(new URL('./cli.js', import.meta.url)), 'native', 'mcp', '--root', resolve(root)] };
|
|
13
|
+
return harness === 'codex' ? { config: { features: { multi_agent: false, multi_agent_v2: false }, mcp_servers: { tealbrick: server } } } : harness === 'langchain' ? { servers: { tealbrick: server } } : { settings: { hooks: { PreToolUse: [{ matcher: 'mcp__tealbrick__.*', hooks: [{ type: 'command', command: process.execPath, args: [fileURLToPath(new URL('./native-child-guard.js', import.meta.url))] }] }] } }, mcpServers: { tealbrick: server } };
|
|
14
|
+
}
|
|
15
|
+
/** Private setup journal stores only identity intent, never account/runtime credentials. */
|
|
16
|
+
export async function guidedNativeSetup(root, flags, login, prompts) {
|
|
17
|
+
root = await realpath(root);
|
|
18
|
+
const dir = join(root, '.tealbrick');
|
|
19
|
+
try {
|
|
20
|
+
if ((await lstat(dir)).isSymbolicLink())
|
|
21
|
+
throw Error('kit_symlink_path');
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e.code !== 'ENOENT')
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
28
|
+
const paths = ['native-setup.json', 'native-setup.lock', 'native-sdk.json'].map(name => join(dir, name));
|
|
29
|
+
for (const path of paths) {
|
|
30
|
+
try {
|
|
31
|
+
if ((await lstat(path)).isSymbolicLink())
|
|
32
|
+
throw Error('kit_symlink_path');
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
if (e.code !== 'ENOENT')
|
|
36
|
+
throw e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const [journalPath, lockPath, configPath] = paths;
|
|
40
|
+
try {
|
|
41
|
+
await writeFile(lockPath, String(process.pid), { flag: 'wx', mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (e.code === 'EEXIST')
|
|
45
|
+
throw Error('kit_native_setup_locked');
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const harness = flags.harness;
|
|
50
|
+
if (harness === 'codex' && flags.delegation !== 'disabled')
|
|
51
|
+
throw Error('kit_native_codex_delegation_opt_in_required');
|
|
52
|
+
if (!Object.hasOwn(harnessLabels, harness))
|
|
53
|
+
throw Error('kit_native_harness_required');
|
|
54
|
+
const old = await readKit(root);
|
|
55
|
+
if (old.runtime && (old.runtime.issuer !== login.issuer || old.runtime.org !== login.org) || old.native && old.native.harness !== harness)
|
|
56
|
+
throw Error('kit_runtime_existing_binding_mismatch');
|
|
57
|
+
if (old.portal || old.voice || old.vision || old.deliver !== undefined || old.avm || old.routines || old.selfEvolution || old.runtime && !old.native)
|
|
58
|
+
throw Error('kit_native_eve_configuration_conflict');
|
|
59
|
+
let journal;
|
|
60
|
+
try {
|
|
61
|
+
journal = JSON.parse(await readFile(journalPath, 'utf8'));
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (e.code !== 'ENOENT')
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
const stamp = { issuer: login.issuer, org: login.org, userId: login.userId, harness };
|
|
68
|
+
if (journal && JSON.stringify(journal.stamp) !== JSON.stringify(stamp))
|
|
69
|
+
throw Error('kit_native_setup_account_mismatch');
|
|
70
|
+
let request = journal?.status !== 'rejected' ? journal?.request : undefined;
|
|
71
|
+
let resuming = !!request;
|
|
72
|
+
let selectionFlags = { ...flags };
|
|
73
|
+
if (request) {
|
|
74
|
+
// A prior POST may already have created the identity. Reconcile that exact key first.
|
|
75
|
+
const mismatched = (flags.agent && request.agent.id !== flags.agent) || (flags['create-agent'] && request.agent.mode === 'create' && request.agent.name !== flags['create-agent'].trim()) || (flags.workspace && request.workspace.id !== flags.workspace) || (flags['create-workspace'] && request.workspace.name !== flags['create-workspace'].trim());
|
|
76
|
+
if (mismatched)
|
|
77
|
+
throw Error('kit_native_setup_pending_intent_mismatch');
|
|
78
|
+
}
|
|
79
|
+
const saveJournal = async (status) => { const tmp = journalPath + '.' + randomUUID(); await writeFile(tmp, JSON.stringify({ version: 1, stamp, request, status }) + '\n', { flag: 'wx', mode: 0o600 }); await rename(tmp, journalPath); };
|
|
80
|
+
let prepared;
|
|
81
|
+
for (let attempt = 0;; attempt++) {
|
|
82
|
+
if (!request) {
|
|
83
|
+
const chosen = await selectNativeIdentity(root, selectionFlags, login, prompts);
|
|
84
|
+
const agent = 'id' in chosen.agent ? { mode: 'attach', id: chosen.agent.id, harness: harnessLabels[harness] } : { mode: 'create', name: chosen.agent.name, harness: harnessLabels[harness] };
|
|
85
|
+
if (old.runtime && (!('id' in chosen.workspace) || chosen.workspace.id !== old.runtime.workspaceId || !('id' in chosen.agent) || chosen.agent.id !== old.runtime.agentId))
|
|
86
|
+
throw Error('kit_runtime_existing_binding_mismatch');
|
|
87
|
+
request = { workspace: chosen.workspace, agent, idempotencyKey: randomUUID() };
|
|
88
|
+
await saveJournal('pending');
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
prepared = await login.client.request('/api/runtime/setup', request);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// This exact Portal error occurs before workspace/agent mutation. Other failures
|
|
96
|
+
// retain the pending request, including timeouts, lost responses and HTTP 5xx.
|
|
97
|
+
if (!(error instanceof PortalHttpError) || error.status !== 409 || error.code !== 'agent_name_exists_select_existing' || attempt >= 3 || request.agent.mode !== 'create')
|
|
98
|
+
throw error;
|
|
99
|
+
await saveJournal('rejected');
|
|
100
|
+
resuming = false;
|
|
101
|
+
selectionFlags = { harness, ...(request.workspace.id ? { workspace: request.workspace.id } : { 'create-workspace': request.workspace.name }), 'create-agent': request.agent.name };
|
|
102
|
+
request = undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!prepared.workspace?.id || !prepared.agent?.id || !prepared.nodeId || prepared.agent.harness !== harnessLabels[harness])
|
|
106
|
+
throw Error('kit_native_setup_response_invalid');
|
|
107
|
+
if (resuming && ((flags['create-agent'] && prepared.agent.name !== flags['create-agent'].trim()) || (flags['agent-name'] && prepared.agent.name !== flags['agent-name']) || (flags['workspace-name'] && prepared.workspace.name !== flags['workspace-name'])))
|
|
108
|
+
throw Error('kit_native_setup_pending_intent_mismatch');
|
|
109
|
+
if (flags.node && flags.node !== prepared.nodeId)
|
|
110
|
+
throw Error('kit_owned_agent_card_required');
|
|
111
|
+
await saveJournal('prepared');
|
|
112
|
+
const authenticated = { ...login, workspaces: [...login.workspaces.filter(w => w.id !== prepared.workspace.id), prepared.workspace] };
|
|
113
|
+
const result = await setupNative(root, { harness, workspaceId: prepared.workspace.id, nodeId: prepared.nodeId, agentId: prepared.agent.id }, authenticated);
|
|
114
|
+
const tmp = configPath + '.' + randomUUID();
|
|
115
|
+
await writeFile(tmp, JSON.stringify(nativeSdkConfig(harness, root, { codexDelegationDisabled: flags.delegation === 'disabled' }), null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
116
|
+
await rename(tmp, configPath);
|
|
117
|
+
return { ...result, workspaceId: prepared.workspace.id, nodeId: prepared.nodeId, configPath };
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await unlink(lockPath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ClaudeProfile, StandingGrant } from './native-serve-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Standing grants: owner-defined pre-approvals for bounded classes of approval-gated Teal Brick calls.
|
|
4
|
+
* The kit's PreToolUse hook (never the model) decides: a matching, unexpired grant with remaining caps
|
|
5
|
+
* replaces the per-payload prompt. Anything else falls back to the normal payload approval.
|
|
6
|
+
*
|
|
7
|
+
* Usage counters live in `<state>/standing-grants.json` (0600): only timestamps per grant id, pruned to
|
|
8
|
+
* the 24h window. `native serve` and `native acp` on the same kit root share the file. Every
|
|
9
|
+
* check-and-record runs under an exclusive lock file created with O_EXCL (`<file>.lock`), retried until
|
|
10
|
+
* a timeout; a lock older than staleLockMs is treated as abandoned (crashed holder) and removed. Writes are
|
|
11
|
+
* tmp + rename, so readers never see a partial file. If the lock cannot be taken or the file is unreadable,
|
|
12
|
+
* no standing approval is given (fail closed to the prompt).
|
|
13
|
+
*/
|
|
14
|
+
export type GrantRefusal = 'per_day' | 'per_hour' | 'min_interval' | 'store_unavailable';
|
|
15
|
+
export interface GrantUsageStore {
|
|
16
|
+
now(): number;
|
|
17
|
+
/** Atomically checks the grant's caps and records one use when they allow it. */
|
|
18
|
+
consume(grant: StandingGrant): Promise<{
|
|
19
|
+
ok: true;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
reason: GrantRefusal;
|
|
23
|
+
}>;
|
|
24
|
+
/** Read-only usage snapshot (timestamps per grant id), or undefined when unreadable. */
|
|
25
|
+
usage(): Promise<Record<string, number[]> | undefined>;
|
|
26
|
+
}
|
|
27
|
+
export declare function capRefusal(grant: StandingGrant, stamps: number[], now: number): GrantRefusal | undefined;
|
|
28
|
+
export interface GrantStoreOptions {
|
|
29
|
+
now?: () => number;
|
|
30
|
+
lockTimeoutMs?: number;
|
|
31
|
+
staleLockMs?: number;
|
|
32
|
+
}
|
|
33
|
+
/** Persistent store at `file`, or an in-memory store when `file` is undefined (harness-only tests). */
|
|
34
|
+
export declare function standingGrantStore(path: string | undefined, options?: GrantStoreOptions): GrantUsageStore;
|
|
35
|
+
export declare const grantExpired: (grant: StandingGrant, now: number) => boolean;
|
|
36
|
+
export type GrantRef = {
|
|
37
|
+
appId: string;
|
|
38
|
+
plugin?: string;
|
|
39
|
+
action?: string;
|
|
40
|
+
};
|
|
41
|
+
export interface GrantCall {
|
|
42
|
+
name: string;
|
|
43
|
+
input: Record<string, unknown>;
|
|
44
|
+
agentId?: string;
|
|
45
|
+
agentType?: string;
|
|
46
|
+
}
|
|
47
|
+
export type GrantDecision = {
|
|
48
|
+
grant: StandingGrant;
|
|
49
|
+
ref?: GrantRef;
|
|
50
|
+
};
|
|
51
|
+
export interface GrantContext {
|
|
52
|
+
profile: ClaudeProfile;
|
|
53
|
+
store: GrantUsageStore;
|
|
54
|
+
resolveApp?: (registrationId: string) => Promise<GrantRef | undefined>;
|
|
55
|
+
trace: (record: Record<string, unknown>) => void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Finds the first configured grant (in order) that covers this approval-gated call and still has capacity,
|
|
59
|
+
* and records its use. Returns undefined when the normal payload approval must run.
|
|
60
|
+
*/
|
|
61
|
+
export declare function standingGrantFor(ctx: GrantContext, call: GrantCall): Promise<GrantDecision | undefined>;
|
|
62
|
+
/** Owner-facing audit view: no deny patterns or inputs, only identifiers, caps, expiry and remaining uses. */
|
|
63
|
+
export declare function standingGrantSummary(grants: StandingGrant[], store: GrantUsageStore): Promise<({
|
|
64
|
+
denyPatterns: number;
|
|
65
|
+
remainingThisHour?: number | undefined;
|
|
66
|
+
usedToday: number;
|
|
67
|
+
remainingToday: number;
|
|
68
|
+
id: string;
|
|
69
|
+
description: string;
|
|
70
|
+
operation: string;
|
|
71
|
+
match: {
|
|
72
|
+
registrationId?: string | undefined;
|
|
73
|
+
plugin?: string | string[] | undefined;
|
|
74
|
+
action?: string | string[] | undefined;
|
|
75
|
+
};
|
|
76
|
+
experts: string[];
|
|
77
|
+
caps: {
|
|
78
|
+
perDay: number;
|
|
79
|
+
perHour?: number | undefined;
|
|
80
|
+
minIntervalSeconds?: number | undefined;
|
|
81
|
+
};
|
|
82
|
+
expires: string;
|
|
83
|
+
expired: boolean;
|
|
84
|
+
} | {
|
|
85
|
+
denyPatterns: number;
|
|
86
|
+
usage: string;
|
|
87
|
+
id: string;
|
|
88
|
+
description: string;
|
|
89
|
+
operation: string;
|
|
90
|
+
match: {
|
|
91
|
+
registrationId?: string | undefined;
|
|
92
|
+
plugin?: string | string[] | undefined;
|
|
93
|
+
action?: string | string[] | undefined;
|
|
94
|
+
};
|
|
95
|
+
experts: string[];
|
|
96
|
+
caps: {
|
|
97
|
+
perDay: number;
|
|
98
|
+
perHour?: number | undefined;
|
|
99
|
+
minIntervalSeconds?: number | undefined;
|
|
100
|
+
};
|
|
101
|
+
expires: string;
|
|
102
|
+
expired: boolean;
|
|
103
|
+
})[]>;
|
|
104
|
+
/** System-prompt note for the owner session; empty when no unexpired grant exists. */
|
|
105
|
+
export declare function standingGrantNote(grants: StandingGrant[], now: number): string;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { lstat, mkdir, open, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
const HOUR = 3_600_000, DAY = 24 * HOUR;
|
|
5
|
+
export function capRefusal(grant, stamps, now) {
|
|
6
|
+
const day = stamps.filter(t => t > now - DAY && t <= now);
|
|
7
|
+
if (day.length >= grant.caps.perDay)
|
|
8
|
+
return 'per_day';
|
|
9
|
+
if (grant.caps.perHour !== undefined && day.filter(t => t > now - HOUR).length >= grant.caps.perHour)
|
|
10
|
+
return 'per_hour';
|
|
11
|
+
if (grant.caps.minIntervalSeconds !== undefined && day.some(t => t > now - grant.caps.minIntervalSeconds * 1000))
|
|
12
|
+
return 'min_interval';
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const prune = (usage, now) => {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [id, stamps] of Object.entries(usage)) {
|
|
18
|
+
const kept = stamps.filter(t => t > now - DAY).sort((a, b) => a - b).slice(-200);
|
|
19
|
+
if (kept.length)
|
|
20
|
+
out[id] = kept;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
};
|
|
24
|
+
function parseUsage(text) {
|
|
25
|
+
const value = JSON.parse(text);
|
|
26
|
+
if (!value || value.schema !== 1 || typeof value.usage !== 'object' || value.usage === null || Array.isArray(value.usage))
|
|
27
|
+
throw Error('kit_standing_grants_store_invalid');
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const [id, stamps] of Object.entries(value.usage)) {
|
|
30
|
+
if (!/^[a-z0-9][a-z0-9-]{1,47}$/.test(id) || !Array.isArray(stamps) || !stamps.every(t => Number.isFinite(t)))
|
|
31
|
+
throw Error('kit_standing_grants_store_invalid');
|
|
32
|
+
out[id] = stamps;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
37
|
+
/** Persistent store at `file`, or an in-memory store when `file` is undefined (harness-only tests). */
|
|
38
|
+
export function standingGrantStore(path, options = {}) {
|
|
39
|
+
const now = options.now ?? Date.now, lockTimeout = options.lockTimeoutMs ?? 3000, stale = options.staleLockMs ?? 30_000;
|
|
40
|
+
if (!path) {
|
|
41
|
+
const memory = {};
|
|
42
|
+
return { now, async usage() { return prune(memory, now()); },
|
|
43
|
+
async consume(grant) { const t = now(), stamps = memory[grant.id] ?? [], why = capRefusal(grant, stamps, t); if (why)
|
|
44
|
+
return { ok: false, reason: why }; memory[grant.id] = [...stamps.filter(s => s > t - DAY), t]; return { ok: true }; } };
|
|
45
|
+
}
|
|
46
|
+
const file = path, lock = file + '.lock';
|
|
47
|
+
// In-process serialisation keeps same-process callers from contending on the lock file.
|
|
48
|
+
let chain = Promise.resolve();
|
|
49
|
+
const read = async () => {
|
|
50
|
+
try {
|
|
51
|
+
const s = await lstat(file);
|
|
52
|
+
if (!s.isFile() || s.isSymbolicLink())
|
|
53
|
+
throw Error('kit_standing_grants_store_unsafe');
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (e.code === 'ENOENT')
|
|
57
|
+
return {};
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
return parseUsage(await readFile(file, 'utf8'));
|
|
61
|
+
};
|
|
62
|
+
async function acquire() {
|
|
63
|
+
const deadline = Date.now() + lockTimeout;
|
|
64
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
65
|
+
for (;;) {
|
|
66
|
+
try {
|
|
67
|
+
const h = await open(lock, 'wx', 0o600);
|
|
68
|
+
await h.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
69
|
+
await h.close();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (e.code !== 'EEXIST')
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
if (Date.now() - (await stat(lock)).mtimeMs > stale)
|
|
78
|
+
await unlink(lock).catch(() => { });
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
if (Date.now() > deadline)
|
|
82
|
+
return false;
|
|
83
|
+
await sleep(15 + Math.random() * 20);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function locked(fn, busy) {
|
|
87
|
+
const run = chain.then(async () => {
|
|
88
|
+
try {
|
|
89
|
+
if (!await acquire())
|
|
90
|
+
return busy;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return busy;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return await fn();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return busy;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await unlink(lock).catch(() => { });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
chain = run;
|
|
106
|
+
return run;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
now,
|
|
110
|
+
async usage() { try {
|
|
111
|
+
return prune(await read(), now());
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
} },
|
|
116
|
+
consume(grant) {
|
|
117
|
+
return locked(async () => {
|
|
118
|
+
let usage;
|
|
119
|
+
try {
|
|
120
|
+
usage = await read();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { ok: false, reason: 'store_unavailable' };
|
|
124
|
+
}
|
|
125
|
+
const t = now();
|
|
126
|
+
usage = prune(usage, t);
|
|
127
|
+
const why = capRefusal(grant, usage[grant.id] ?? [], t);
|
|
128
|
+
if (why)
|
|
129
|
+
return { ok: false, reason: why };
|
|
130
|
+
usage[grant.id] = [...(usage[grant.id] ?? []), t];
|
|
131
|
+
const tmp = file + '.' + randomUUID();
|
|
132
|
+
try {
|
|
133
|
+
await writeFile(tmp, JSON.stringify({ schema: 1, usage }) + '\n', { flag: 'wx', mode: 0o600 });
|
|
134
|
+
await rename(tmp, file);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
await unlink(tmp).catch(() => { });
|
|
138
|
+
return { ok: false, reason: 'store_unavailable' };
|
|
139
|
+
}
|
|
140
|
+
return { ok: true };
|
|
141
|
+
}, { ok: false, reason: 'store_unavailable' });
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const norm = (op) => op.replaceAll('.', '_');
|
|
146
|
+
const globMatch = (pattern, value, ci) => {
|
|
147
|
+
const re = new RegExp('^' + pattern.split('*').map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$', ci ? 'is' : 's');
|
|
148
|
+
return re.test(value);
|
|
149
|
+
};
|
|
150
|
+
const anyGlob = (patterns, value, ci) => value !== undefined && (Array.isArray(patterns) ? patterns : [patterns]).some(p => globMatch(p, value, ci));
|
|
151
|
+
export const grantExpired = (grant, now) => !(Date.parse(grant.expires) > now);
|
|
152
|
+
/**
|
|
153
|
+
* Finds the first configured grant (in order) that covers this approval-gated call and still has capacity,
|
|
154
|
+
* and records its use. Returns undefined when the normal payload approval must run.
|
|
155
|
+
*/
|
|
156
|
+
export async function standingGrantFor(ctx, call) {
|
|
157
|
+
const { profile, store } = ctx, { name, input } = call;
|
|
158
|
+
if (!profile.standingGrants.length || name !== 'mcp__tealbrick__tealbrick_call' || typeof input.operation !== 'string' || typeof input.registrationId !== 'string')
|
|
159
|
+
return undefined;
|
|
160
|
+
const op = norm(input.operation), registrationId = input.registrationId, child = call.agentId !== undefined;
|
|
161
|
+
let ref, resolved = false;
|
|
162
|
+
const resolve = async () => { if (!resolved) {
|
|
163
|
+
resolved = true;
|
|
164
|
+
try {
|
|
165
|
+
ref = await ctx.resolveApp?.(registrationId);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
ref = undefined;
|
|
169
|
+
}
|
|
170
|
+
} return ref; };
|
|
171
|
+
const body = JSON.stringify(input.input ?? null).toLowerCase();
|
|
172
|
+
for (const grant of profile.standingGrants) {
|
|
173
|
+
if (norm(grant.operation) !== op)
|
|
174
|
+
continue;
|
|
175
|
+
if (child && !(call.agentType !== undefined && grant.experts?.includes(call.agentType) && Object.hasOwn(profile.contracts, call.agentType)))
|
|
176
|
+
continue;
|
|
177
|
+
const m = grant.match;
|
|
178
|
+
if (m.registrationId !== undefined && !globMatch(m.registrationId, registrationId, false))
|
|
179
|
+
continue;
|
|
180
|
+
if (m.plugin !== undefined || m.action !== undefined) {
|
|
181
|
+
const r = await resolve();
|
|
182
|
+
if (!r || r.plugin === undefined || r.action === undefined)
|
|
183
|
+
continue;
|
|
184
|
+
if (m.plugin !== undefined && !anyGlob(m.plugin, r.plugin, true))
|
|
185
|
+
continue;
|
|
186
|
+
if (m.action !== undefined && !anyGlob(m.action, r.action, true))
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (grant.deny?.inputPattern?.some(p => body.includes(p.toLowerCase()))) {
|
|
190
|
+
ctx.trace({ event: 'standing_grant.skipped', grantId: grant.id, reason: 'deny_pattern', operation: input.operation, registrationId });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (grantExpired(grant, store.now())) {
|
|
194
|
+
ctx.trace({ event: 'standing_grant.exhausted', grantId: grant.id, reason: 'expired', operation: input.operation, registrationId });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const used = await store.consume(grant);
|
|
198
|
+
if (!used.ok) {
|
|
199
|
+
ctx.trace({ event: 'standing_grant.exhausted', grantId: grant.id, reason: used.reason, operation: input.operation, registrationId });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
return { grant, ...(ref ? { ref } : {}) };
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
/** Owner-facing audit view: no deny patterns or inputs, only identifiers, caps, expiry and remaining uses. */
|
|
207
|
+
export async function standingGrantSummary(grants, store) {
|
|
208
|
+
const usage = await store.usage(), now = store.now();
|
|
209
|
+
return grants.map(g => {
|
|
210
|
+
const stamps = (usage?.[g.id] ?? []).filter(t => t > now - DAY && t <= now), hour = stamps.filter(t => t > now - HOUR).length;
|
|
211
|
+
return { id: g.id, description: g.description, operation: g.operation, match: g.match, experts: [...(g.experts ?? [])], caps: g.caps, expires: g.expires,
|
|
212
|
+
expired: grantExpired(g, now), ...(usage ? { usedToday: stamps.length, remainingToday: Math.max(0, g.caps.perDay - stamps.length),
|
|
213
|
+
...(g.caps.perHour !== undefined ? { remainingThisHour: Math.max(0, g.caps.perHour - hour) } : {}) } : { usage: 'unavailable' }),
|
|
214
|
+
denyPatterns: g.deny?.inputPattern?.length ?? 0 };
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/** System-prompt note for the owner session; empty when no unexpired grant exists. */
|
|
218
|
+
export function standingGrantNote(grants, now) {
|
|
219
|
+
const live = grants.filter(g => !grantExpired(g, now));
|
|
220
|
+
if (!live.length)
|
|
221
|
+
return '';
|
|
222
|
+
const caps = (g) => [`${g.caps.perDay}/day`, ...(g.caps.perHour !== undefined ? [`${g.caps.perHour}/hour`] : []), ...(g.caps.minIntervalSeconds !== undefined ? [`at least ${g.caps.minIntervalSeconds}s apart`] : [])].join(', ');
|
|
223
|
+
return ['Standing grants: your owner pre-approved these classes of calls; the harness applies them automatically (you do not choose them) within their caps until they expire:',
|
|
224
|
+
...live.map(g => `- ${g.id} (${g.operation}): ${g.description} [${caps(g)}; expires ${g.expires}]`),
|
|
225
|
+
'Anything outside these grants, or beyond their caps, still needs the owner\'s approval of the exact payload. Spending money, contracts or signatures, and first contact with strangers are never covered unless a grant explicitly says so.'].join('\n');
|
|
226
|
+
}
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { guidedNativeSetup, nativeSdkConfig } from './native-setup.js';
|
|
2
|
+
import { type PortalLogin } from './onboarding.js';
|
|
3
|
+
import { type KnowledgeOperation } from '@tealbrick/portal/runtime';
|
|
4
|
+
import { MarketplaceRuntimeConnector } from '@tealbrick/portal/marketplace-runtime';
|
|
5
|
+
declare const harnesses: {
|
|
6
|
+
readonly claude: "Claude Agent SDK";
|
|
7
|
+
readonly codex: "Codex";
|
|
8
|
+
readonly langchain: "LangChain";
|
|
9
|
+
};
|
|
10
|
+
export type NativeHarness = keyof typeof harnesses;
|
|
11
|
+
/** Enroll an existing, accurately labelled owned card. Registration belongs to Portal. */
|
|
12
|
+
export declare function setupNative(root: string, input: {
|
|
13
|
+
harness: NativeHarness;
|
|
14
|
+
workspaceId: string;
|
|
15
|
+
nodeId: string;
|
|
16
|
+
agentId: string;
|
|
17
|
+
}, login: PortalLogin): Promise<{
|
|
18
|
+
connected: boolean;
|
|
19
|
+
agentId: string;
|
|
20
|
+
connectionId: string;
|
|
21
|
+
harness: "claude" | "codex" | "langchain";
|
|
22
|
+
}>;
|
|
23
|
+
/** A fresh connector per invocation rechecks Portal and local activation. No authorization cache
|
|
24
|
+
* survives MCP reconnect, local deactivation or a request boundary. No session is created here. */
|
|
25
|
+
export declare function nativeConnector(): {
|
|
26
|
+
marketplace: () => Promise<MarketplaceRuntimeConnector | undefined>;
|
|
27
|
+
capabilities: () => Promise<{
|
|
28
|
+
revision: string;
|
|
29
|
+
expiresAt: number;
|
|
30
|
+
apps: {
|
|
31
|
+
registrationId: string;
|
|
32
|
+
appId: "knowledge";
|
|
33
|
+
actions: import("@tealbrick/portal/runtime").CrudAction[];
|
|
34
|
+
available: boolean;
|
|
35
|
+
operations: KnowledgeOperation[];
|
|
36
|
+
}[];
|
|
37
|
+
}>;
|
|
38
|
+
call: (registrationId: string, operation: KnowledgeOperation, input?: Record<string, unknown>, signal?: AbortSignal) => Promise<any>;
|
|
39
|
+
};
|
|
40
|
+
export declare const nativeUsage = "Usage: tealbrick native setup --harness claude|codex|langchain [--workspace-name NAME | --create-workspace NAME] [--agent-name NAME | --create-agent NAME] [--issuer HTTPS_ORIGIN] | native mcp [--root DIRECTORY] | native serve [--root DIRECTORY] [--install-launchd [--no-load] | --uninstall] | native enroll --url HTTPS_ORIGIN [--port PORT] [--root DIRECTORY] [--skip-endpoint-check] | native acp [--root DIRECTORY] [--buzz-cli PATH] | native enable|disable | native marketplace-enable|marketplace-disable";
|
|
41
|
+
export declare function nativeCommand(args: string[], root: string): Promise<void>;
|
|
42
|
+
export { guidedNativeSetup, nativeSdkConfig };
|