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