@kernel.chat/kbot 4.0.1 → 4.1.0
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/dist/futures/debate/index.d.ts +7 -0
- package/dist/futures/debate/index.js +6 -0
- package/dist/futures/debate/runner.d.ts +34 -0
- package/dist/futures/debate/runner.js +140 -0
- package/dist/futures/debate/synthesis.d.ts +25 -0
- package/dist/futures/debate/synthesis.js +81 -0
- package/dist/futures/debate/types.d.ts +72 -0
- package/dist/futures/debate/types.js +12 -0
- package/dist/futures/forecast/index.d.ts +5 -0
- package/dist/futures/forecast/index.js +5 -0
- package/dist/futures/forecast/projection.d.ts +31 -0
- package/dist/futures/forecast/projection.js +177 -0
- package/dist/futures/forecast/synthesize.d.ts +19 -0
- package/dist/futures/forecast/synthesize.js +89 -0
- package/dist/futures/forecast/types.d.ts +59 -0
- package/dist/futures/forecast/types.js +15 -0
- package/dist/futures/harness/critic-evaluator.d.ts +39 -0
- package/dist/futures/harness/critic-evaluator.js +131 -0
- package/dist/futures/harness/evolution-loop.d.ts +41 -0
- package/dist/futures/harness/evolution-loop.js +168 -0
- package/dist/futures/harness/index.d.ts +16 -0
- package/dist/futures/harness/index.js +13 -0
- package/dist/futures/harness/meta-evolution.d.ts +32 -0
- package/dist/futures/harness/meta-evolution.js +52 -0
- package/dist/futures/harness/noop-evolution.d.ts +23 -0
- package/dist/futures/harness/noop-evolution.js +29 -0
- package/dist/futures/harness/persistence.d.ts +30 -0
- package/dist/futures/harness/persistence.js +99 -0
- package/dist/futures/harness/types.d.ts +147 -0
- package/dist/futures/harness/types.js +18 -0
- package/dist/futures/index.d.ts +16 -0
- package/dist/futures/index.js +22 -0
- package/dist/futures/latent-state/envelope.d.ts +39 -0
- package/dist/futures/latent-state/envelope.js +178 -0
- package/dist/futures/latent-state/index.d.ts +5 -0
- package/dist/futures/latent-state/index.js +3 -0
- package/dist/futures/latent-state/types.d.ts +47 -0
- package/dist/futures/latent-state/types.js +13 -0
- package/dist/futures/persona/check.d.ts +45 -0
- package/dist/futures/persona/check.js +205 -0
- package/dist/futures/persona/index.d.ts +5 -0
- package/dist/futures/persona/index.js +5 -0
- package/dist/futures/persona/registry.d.ts +22 -0
- package/dist/futures/persona/registry.js +124 -0
- package/dist/futures/persona/types.d.ts +68 -0
- package/dist/futures/persona/types.js +28 -0
- package/dist/futures/skill-graph/graph.d.ts +31 -0
- package/dist/futures/skill-graph/graph.js +151 -0
- package/dist/futures/skill-graph/index.d.ts +13 -0
- package/dist/futures/skill-graph/index.js +10 -0
- package/dist/futures/skill-graph/synthesis.d.ts +20 -0
- package/dist/futures/skill-graph/synthesis.js +83 -0
- package/dist/futures/skill-graph/types.d.ts +53 -0
- package/dist/futures/skill-graph/types.js +19 -0
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PermissionGrant, Persona, Verdict } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reset all rate-limit state. Test-only seam; not exported from index.ts.
|
|
4
|
+
*/
|
|
5
|
+
export declare function _resetRateLimits(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve (persona, tool, args) → Verdict.
|
|
8
|
+
*
|
|
9
|
+
* Iterates scopes in order; the first scope whose toolPattern matches and
|
|
10
|
+
* whose argConstraints + rateLimit + blast-radius all pass produces an
|
|
11
|
+
* `allowed: true` verdict. If a scope's toolPattern matches but a check
|
|
12
|
+
* fails, we continue checking later scopes — a denial on one scope doesn't
|
|
13
|
+
* forbid a later, more permissive scope.
|
|
14
|
+
*
|
|
15
|
+
* If no scope matches the tool name at all, the verdict is denied with
|
|
16
|
+
* reason "no scope matched". If scopes matched but all failed sub-checks,
|
|
17
|
+
* the verdict is denied with the *last* failure reason.
|
|
18
|
+
*/
|
|
19
|
+
export declare function canInvoke(persona: Persona, toolName: string, args: Record<string, unknown>, opts?: {
|
|
20
|
+
now?: number;
|
|
21
|
+
}): Verdict;
|
|
22
|
+
/**
|
|
23
|
+
* Throws PermissionDeniedError if canInvoke yields a denied verdict.
|
|
24
|
+
* Returns the verdict on success for callers that want to inspect matchedScope.
|
|
25
|
+
*/
|
|
26
|
+
export declare function enforce(grant: PermissionGrant, opts?: {
|
|
27
|
+
now?: number;
|
|
28
|
+
}): Verdict;
|
|
29
|
+
/**
|
|
30
|
+
* Compose multiple personas into a single one.
|
|
31
|
+
* - id: joined with "+"
|
|
32
|
+
* - description: joined with "; "
|
|
33
|
+
* - scopes: concatenated (canInvoke iterates in order, so earliest wins ties)
|
|
34
|
+
* - maxBlastRadius: max over all inputs (undefined treated as 'none')
|
|
35
|
+
*
|
|
36
|
+
* Rate-limit state is keyed on the *resulting* persona's id, so merged
|
|
37
|
+
* personas have their own counter independent of the inputs.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mergePersonas(...personas: Persona[]): Persona;
|
|
40
|
+
/**
|
|
41
|
+
* Lookup helper. Throws on miss so callers fail loudly rather than silently
|
|
42
|
+
* proceeding without a scope.
|
|
43
|
+
*/
|
|
44
|
+
export declare function loadPersona(id: string, registry: Record<string, Persona>): Persona;
|
|
45
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// futures/persona/check — runtime resolver for (persona, tool, args) → Verdict.
|
|
2
|
+
//
|
|
3
|
+
// Pure resolution + a module-level Map for rate-limit counters. The Map is
|
|
4
|
+
// process-scoped, which is correct for a single CLI run; longer-lived state
|
|
5
|
+
// (e.g. across daemon restarts) is out of scope for v5 phase 1.
|
|
6
|
+
import { BLAST_RADIUS_ORDER, PermissionDeniedError, } from './types.js';
|
|
7
|
+
/** keyed by `${persona.id}::${toolName}` */
|
|
8
|
+
const RATE_LIMIT_STATE = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Reset all rate-limit state. Test-only seam; not exported from index.ts.
|
|
11
|
+
*/
|
|
12
|
+
export function _resetRateLimits() {
|
|
13
|
+
RATE_LIMIT_STATE.clear();
|
|
14
|
+
}
|
|
15
|
+
function rateKey(personaId, toolName) {
|
|
16
|
+
return `${personaId}::${toolName}`;
|
|
17
|
+
}
|
|
18
|
+
function blastRadiusRank(r) {
|
|
19
|
+
return BLAST_RADIUS_ORDER.indexOf(r);
|
|
20
|
+
}
|
|
21
|
+
function maxBlastRadius(a, b) {
|
|
22
|
+
return blastRadiusRank(a) >= blastRadiusRank(b) ? a : b;
|
|
23
|
+
}
|
|
24
|
+
function patternMatches(pattern, toolName) {
|
|
25
|
+
if (typeof pattern === 'string')
|
|
26
|
+
return pattern === toolName;
|
|
27
|
+
return pattern.test(toolName);
|
|
28
|
+
}
|
|
29
|
+
function checkArgRule(argName, value, rule) {
|
|
30
|
+
// type
|
|
31
|
+
if (rule.type === 'enum') {
|
|
32
|
+
if (!rule.allowedValues || !rule.allowedValues.includes(value)) {
|
|
33
|
+
return { ok: false, reason: `arg "${argName}" not in allowedValues` };
|
|
34
|
+
}
|
|
35
|
+
return { ok: true };
|
|
36
|
+
}
|
|
37
|
+
if (rule.type === 'string') {
|
|
38
|
+
if (typeof value !== 'string')
|
|
39
|
+
return { ok: false, reason: `arg "${argName}" must be string` };
|
|
40
|
+
if (rule.pattern) {
|
|
41
|
+
const matched = rule.pattern.test(value);
|
|
42
|
+
if (rule.denyPattern && matched) {
|
|
43
|
+
return { ok: false, reason: `arg "${argName}" matches deny pattern` };
|
|
44
|
+
}
|
|
45
|
+
if (!rule.denyPattern && !matched) {
|
|
46
|
+
return { ok: false, reason: `arg "${argName}" does not match required pattern` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (rule.min !== undefined && value.length < rule.min) {
|
|
50
|
+
return { ok: false, reason: `arg "${argName}" shorter than min=${rule.min}` };
|
|
51
|
+
}
|
|
52
|
+
if (rule.max !== undefined && value.length > rule.max) {
|
|
53
|
+
return { ok: false, reason: `arg "${argName}" longer than max=${rule.max}` };
|
|
54
|
+
}
|
|
55
|
+
return { ok: true };
|
|
56
|
+
}
|
|
57
|
+
if (rule.type === 'number') {
|
|
58
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
59
|
+
return { ok: false, reason: `arg "${argName}" must be number` };
|
|
60
|
+
}
|
|
61
|
+
if (rule.min !== undefined && value < rule.min) {
|
|
62
|
+
return { ok: false, reason: `arg "${argName}" below min=${rule.min}` };
|
|
63
|
+
}
|
|
64
|
+
if (rule.max !== undefined && value > rule.max) {
|
|
65
|
+
return { ok: false, reason: `arg "${argName}" above max=${rule.max}` };
|
|
66
|
+
}
|
|
67
|
+
if (rule.allowedValues && !rule.allowedValues.includes(value)) {
|
|
68
|
+
return { ok: false, reason: `arg "${argName}" not in allowedValues` };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true };
|
|
71
|
+
}
|
|
72
|
+
if (rule.type === 'boolean') {
|
|
73
|
+
if (typeof value !== 'boolean')
|
|
74
|
+
return { ok: false, reason: `arg "${argName}" must be boolean` };
|
|
75
|
+
if (rule.allowedValues && !rule.allowedValues.includes(value)) {
|
|
76
|
+
return { ok: false, reason: `arg "${argName}" not in allowedValues` };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
80
|
+
return { ok: false, reason: `arg "${argName}" unknown rule type` };
|
|
81
|
+
}
|
|
82
|
+
function checkArgs(scope, args) {
|
|
83
|
+
if (!scope.argConstraints)
|
|
84
|
+
return { ok: true };
|
|
85
|
+
for (const [argName, rule] of Object.entries(scope.argConstraints)) {
|
|
86
|
+
const result = checkArgRule(argName, args[argName], rule);
|
|
87
|
+
if (!result.ok)
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
return { ok: true };
|
|
91
|
+
}
|
|
92
|
+
function checkRateLimit(personaId, toolName, scope, now) {
|
|
93
|
+
if (!scope.rateLimit)
|
|
94
|
+
return { ok: true };
|
|
95
|
+
const { max, windowMs } = scope.rateLimit;
|
|
96
|
+
const key = rateKey(personaId, toolName);
|
|
97
|
+
const bucket = RATE_LIMIT_STATE.get(key) ?? { hits: [] };
|
|
98
|
+
// drop hits outside the window
|
|
99
|
+
bucket.hits = bucket.hits.filter((ts) => now - ts < windowMs);
|
|
100
|
+
if (bucket.hits.length >= max) {
|
|
101
|
+
RATE_LIMIT_STATE.set(key, bucket);
|
|
102
|
+
return { ok: false, reason: `rate limit exceeded (${max}/${windowMs}ms)` };
|
|
103
|
+
}
|
|
104
|
+
bucket.hits.push(now);
|
|
105
|
+
RATE_LIMIT_STATE.set(key, bucket);
|
|
106
|
+
return { ok: true };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve (persona, tool, args) → Verdict.
|
|
110
|
+
*
|
|
111
|
+
* Iterates scopes in order; the first scope whose toolPattern matches and
|
|
112
|
+
* whose argConstraints + rateLimit + blast-radius all pass produces an
|
|
113
|
+
* `allowed: true` verdict. If a scope's toolPattern matches but a check
|
|
114
|
+
* fails, we continue checking later scopes — a denial on one scope doesn't
|
|
115
|
+
* forbid a later, more permissive scope.
|
|
116
|
+
*
|
|
117
|
+
* If no scope matches the tool name at all, the verdict is denied with
|
|
118
|
+
* reason "no scope matched". If scopes matched but all failed sub-checks,
|
|
119
|
+
* the verdict is denied with the *last* failure reason.
|
|
120
|
+
*/
|
|
121
|
+
export function canInvoke(persona, toolName, args, opts) {
|
|
122
|
+
const now = opts?.now ?? Date.now();
|
|
123
|
+
let lastFailure = null;
|
|
124
|
+
let anyToolMatch = false;
|
|
125
|
+
const personaCap = persona.maxBlastRadius;
|
|
126
|
+
for (const scope of persona.scopes) {
|
|
127
|
+
if (!patternMatches(scope.toolPattern, toolName))
|
|
128
|
+
continue;
|
|
129
|
+
anyToolMatch = true;
|
|
130
|
+
// enforce blast-radius cap
|
|
131
|
+
const scopeRadius = scope.blastRadius ?? personaCap ?? 'none';
|
|
132
|
+
if (personaCap && blastRadiusRank(scopeRadius) > blastRadiusRank(personaCap)) {
|
|
133
|
+
lastFailure = {
|
|
134
|
+
reason: `scope blastRadius=${scopeRadius} exceeds persona max=${personaCap}`,
|
|
135
|
+
scope,
|
|
136
|
+
};
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const argResult = checkArgs(scope, args);
|
|
140
|
+
if (!argResult.ok) {
|
|
141
|
+
lastFailure = { reason: argResult.reason, scope };
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const rateResult = checkRateLimit(persona.id, toolName, scope, now);
|
|
145
|
+
if (!rateResult.ok) {
|
|
146
|
+
lastFailure = { reason: rateResult.reason, scope };
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
return { allowed: true, matchedScope: scope };
|
|
150
|
+
}
|
|
151
|
+
if (lastFailure) {
|
|
152
|
+
return { allowed: false, reason: lastFailure.reason, matchedScope: lastFailure.scope };
|
|
153
|
+
}
|
|
154
|
+
if (!anyToolMatch) {
|
|
155
|
+
return { allowed: false, reason: `no scope matched tool "${toolName}"` };
|
|
156
|
+
}
|
|
157
|
+
return { allowed: false, reason: 'permission denied' };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Throws PermissionDeniedError if canInvoke yields a denied verdict.
|
|
161
|
+
* Returns the verdict on success for callers that want to inspect matchedScope.
|
|
162
|
+
*/
|
|
163
|
+
export function enforce(grant, opts) {
|
|
164
|
+
const verdict = canInvoke(grant.persona, grant.toolName, grant.args, opts);
|
|
165
|
+
if (!verdict.allowed)
|
|
166
|
+
throw new PermissionDeniedError(grant, verdict);
|
|
167
|
+
return verdict;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Compose multiple personas into a single one.
|
|
171
|
+
* - id: joined with "+"
|
|
172
|
+
* - description: joined with "; "
|
|
173
|
+
* - scopes: concatenated (canInvoke iterates in order, so earliest wins ties)
|
|
174
|
+
* - maxBlastRadius: max over all inputs (undefined treated as 'none')
|
|
175
|
+
*
|
|
176
|
+
* Rate-limit state is keyed on the *resulting* persona's id, so merged
|
|
177
|
+
* personas have their own counter independent of the inputs.
|
|
178
|
+
*/
|
|
179
|
+
export function mergePersonas(...personas) {
|
|
180
|
+
if (personas.length === 0) {
|
|
181
|
+
return { id: 'empty', description: 'empty merged persona', scopes: [], maxBlastRadius: 'none' };
|
|
182
|
+
}
|
|
183
|
+
if (personas.length === 1)
|
|
184
|
+
return personas[0];
|
|
185
|
+
const id = personas.map((p) => p.id).join('+');
|
|
186
|
+
const description = personas.map((p) => p.description).join('; ');
|
|
187
|
+
const scopes = personas.flatMap((p) => p.scopes);
|
|
188
|
+
let cap = 'none';
|
|
189
|
+
for (const p of personas) {
|
|
190
|
+
if (p.maxBlastRadius)
|
|
191
|
+
cap = maxBlastRadius(cap, p.maxBlastRadius);
|
|
192
|
+
}
|
|
193
|
+
return { id, description, scopes, maxBlastRadius: cap };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Lookup helper. Throws on miss so callers fail loudly rather than silently
|
|
197
|
+
* proceeding without a scope.
|
|
198
|
+
*/
|
|
199
|
+
export function loadPersona(id, registry) {
|
|
200
|
+
const found = registry[id];
|
|
201
|
+
if (!found)
|
|
202
|
+
throw new Error(`unknown persona id "${id}"`);
|
|
203
|
+
return found;
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { ArgRule, BlastRadius, PermissionGrant, Persona, Scope, Verdict, } from './types.js';
|
|
2
|
+
export { BLAST_RADIUS_ORDER, PermissionDeniedError } from './types.js';
|
|
3
|
+
export { canInvoke, enforce, mergePersonas, loadPersona } from './check.js';
|
|
4
|
+
export { RESEARCHER, CODER, COMPUTER_USE, PERSONA_REGISTRY } from './registry.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// futures/persona — public surface.
|
|
2
|
+
export { BLAST_RADIUS_ORDER, PermissionDeniedError } from './types.js';
|
|
3
|
+
export { canInvoke, enforce, mergePersonas, loadPersona } from './check.js';
|
|
4
|
+
export { RESEARCHER, CODER, COMPUTER_USE, PERSONA_REGISTRY } from './registry.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Persona } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Researcher: read-only research tools. Cannot mutate filesystem, repo, or
|
|
4
|
+
* external state. Web fetches and grep are fine; writes are not.
|
|
5
|
+
*/
|
|
6
|
+
export declare const RESEARCHER: Persona;
|
|
7
|
+
/**
|
|
8
|
+
* Coder: read-write inside the workspace. Bash allowed but the most common
|
|
9
|
+
* destructive forms are denied via deny-pattern argRules. No force pushes.
|
|
10
|
+
* Rate limit on bash so a runaway loop can't burn 10k commands.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CODER: Persona;
|
|
13
|
+
/**
|
|
14
|
+
* Computer-use: explicit destructive opt-in. GUI tools that drive the user's
|
|
15
|
+
* physical desktop. Rate-limited so a hung loop can't spam clicks.
|
|
16
|
+
*/
|
|
17
|
+
export declare const COMPUTER_USE: Persona;
|
|
18
|
+
/**
|
|
19
|
+
* Default registry. Add to this when wiring more personas.
|
|
20
|
+
*/
|
|
21
|
+
export declare const PERSONA_REGISTRY: Record<string, Persona>;
|
|
22
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// futures/persona/registry — example personas. Hand-curated, not auto-generated.
|
|
2
|
+
//
|
|
3
|
+
// These three exist as concrete starting points for integration work; the
|
|
4
|
+
// next session will wire them into permissions.ts. Until then they're
|
|
5
|
+
// import-and-use values for tests and ad-hoc experimentation.
|
|
6
|
+
/**
|
|
7
|
+
* Researcher: read-only research tools. Cannot mutate filesystem, repo, or
|
|
8
|
+
* external state. Web fetches and grep are fine; writes are not.
|
|
9
|
+
*/
|
|
10
|
+
export const RESEARCHER = {
|
|
11
|
+
id: 'researcher',
|
|
12
|
+
description: 'Read-only research and search tools. No filesystem writes, no shell execution.',
|
|
13
|
+
maxBlastRadius: 'read-only',
|
|
14
|
+
scopes: [
|
|
15
|
+
{ toolPattern: 'read_file', blastRadius: 'read-only' },
|
|
16
|
+
{ toolPattern: 'list_directory', blastRadius: 'read-only' },
|
|
17
|
+
{ toolPattern: 'grep', blastRadius: 'read-only' },
|
|
18
|
+
{ toolPattern: 'glob', blastRadius: 'read-only' },
|
|
19
|
+
{ toolPattern: 'web_search', blastRadius: 'read-only' },
|
|
20
|
+
{ toolPattern: 'papers_search', blastRadius: 'read-only' },
|
|
21
|
+
{ toolPattern: 'kbot_search', blastRadius: 'read-only' },
|
|
22
|
+
{ toolPattern: 'arxiv_search', blastRadius: 'read-only' },
|
|
23
|
+
{ toolPattern: /^github_(read_file|repo_info|search|trending|activity|issues)$/, blastRadius: 'read-only' },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Coder: read-write inside the workspace. Bash allowed but the most common
|
|
28
|
+
* destructive forms are denied via deny-pattern argRules. No force pushes.
|
|
29
|
+
* Rate limit on bash so a runaway loop can't burn 10k commands.
|
|
30
|
+
*/
|
|
31
|
+
export const CODER = {
|
|
32
|
+
id: 'coder',
|
|
33
|
+
description: 'Read-write code tools. Bash allowed but rm -rf and force-push denied. Bash 60/min.',
|
|
34
|
+
maxBlastRadius: 'sandboxed',
|
|
35
|
+
scopes: [
|
|
36
|
+
{ toolPattern: 'read_file', blastRadius: 'read-only' },
|
|
37
|
+
{ toolPattern: 'list_directory', blastRadius: 'read-only' },
|
|
38
|
+
{ toolPattern: 'grep', blastRadius: 'read-only' },
|
|
39
|
+
{ toolPattern: 'glob', blastRadius: 'read-only' },
|
|
40
|
+
{ toolPattern: 'write_file', blastRadius: 'sandboxed' },
|
|
41
|
+
{ toolPattern: 'edit_file', blastRadius: 'sandboxed' },
|
|
42
|
+
{ toolPattern: 'multi_file_write', blastRadius: 'sandboxed' },
|
|
43
|
+
{
|
|
44
|
+
toolPattern: 'bash',
|
|
45
|
+
blastRadius: 'sandboxed',
|
|
46
|
+
argConstraints: {
|
|
47
|
+
// deny rm -rf at /, ~, or anywhere with force; deny sudo; deny curl|sh
|
|
48
|
+
command: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
denyPattern: true,
|
|
51
|
+
pattern: /(\brm\s+-[rRfF]+\s+(\/|~|\$HOME)|sudo\s+|curl\s+[^|]*\|\s*(sh|bash)|:\(\)\s*\{)/,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
rateLimit: { max: 60, windowMs: 60_000 },
|
|
55
|
+
},
|
|
56
|
+
{ toolPattern: /^git_(status|log|diff|branch|commit)$/, blastRadius: 'sandboxed' },
|
|
57
|
+
{
|
|
58
|
+
toolPattern: 'git_push',
|
|
59
|
+
blastRadius: 'sandboxed',
|
|
60
|
+
argConstraints: {
|
|
61
|
+
// forbid --force, --force-with-lease, -f
|
|
62
|
+
args: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
denyPattern: true,
|
|
65
|
+
pattern: /(^|\s)(--force(-with-lease)?|-f)(\s|$)/,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{ toolPattern: /^npm_/, blastRadius: 'sandboxed' },
|
|
70
|
+
{ toolPattern: /^pip_/, blastRadius: 'sandboxed' },
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Computer-use: explicit destructive opt-in. GUI tools that drive the user's
|
|
75
|
+
* physical desktop. Rate-limited so a hung loop can't spam clicks.
|
|
76
|
+
*/
|
|
77
|
+
export const COMPUTER_USE = {
|
|
78
|
+
id: 'computer-use',
|
|
79
|
+
description: 'Desktop control: mouse, keyboard, app launch. Destructive blast radius. 30/min.',
|
|
80
|
+
maxBlastRadius: 'destructive',
|
|
81
|
+
scopes: [
|
|
82
|
+
{
|
|
83
|
+
toolPattern: 'mouse_click',
|
|
84
|
+
blastRadius: 'destructive',
|
|
85
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
toolPattern: 'mouse_move',
|
|
89
|
+
blastRadius: 'destructive',
|
|
90
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
toolPattern: 'mouse_drag',
|
|
94
|
+
blastRadius: 'destructive',
|
|
95
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
toolPattern: 'keyboard_type',
|
|
99
|
+
blastRadius: 'destructive',
|
|
100
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
toolPattern: 'keyboard_shortcut',
|
|
104
|
+
blastRadius: 'destructive',
|
|
105
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
toolPattern: 'app_launch',
|
|
109
|
+
blastRadius: 'destructive',
|
|
110
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
111
|
+
},
|
|
112
|
+
{ toolPattern: 'screenshot', blastRadius: 'read-only' },
|
|
113
|
+
{ toolPattern: 'window_list', blastRadius: 'read-only' },
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Default registry. Add to this when wiring more personas.
|
|
118
|
+
*/
|
|
119
|
+
export const PERSONA_REGISTRY = {
|
|
120
|
+
researcher: RESEARCHER,
|
|
121
|
+
coder: CODER,
|
|
122
|
+
'computer-use': COMPUTER_USE,
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blast-radius hierarchy. Ordered from least to most dangerous.
|
|
3
|
+
* A Persona's maxBlastRadius caps the radius any of its scopes may target;
|
|
4
|
+
* mergePersonas() takes the maximum across inputs.
|
|
5
|
+
*/
|
|
6
|
+
export type BlastRadius = 'none' | 'read-only' | 'sandboxed' | 'destructive';
|
|
7
|
+
export declare const BLAST_RADIUS_ORDER: readonly BlastRadius[];
|
|
8
|
+
/**
|
|
9
|
+
* Constraint applied to a single tool argument by name.
|
|
10
|
+
* - `type` — required runtime type
|
|
11
|
+
* - `allowedValues` — enum of permitted literal values (when type === 'enum')
|
|
12
|
+
* - `pattern` — regex test for string args (rejected if it matches a
|
|
13
|
+
* forbidden form; we use it as a *deny* pattern by convention — see check.ts)
|
|
14
|
+
* - `min` / `max` — numeric bounds (inclusive). For strings, applied to length.
|
|
15
|
+
*/
|
|
16
|
+
export interface ArgRule {
|
|
17
|
+
type: 'string' | 'number' | 'boolean' | 'enum';
|
|
18
|
+
allowedValues?: unknown[];
|
|
19
|
+
pattern?: RegExp;
|
|
20
|
+
/** When true, the regex acts as a *deny* pattern (match → reject). Defaults to false (must match). */
|
|
21
|
+
denyPattern?: boolean;
|
|
22
|
+
min?: number;
|
|
23
|
+
max?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A single permission scope. Match against a tool name and a payload of args.
|
|
27
|
+
* - `toolPattern` — exact string match or RegExp test
|
|
28
|
+
* - `argConstraints` — keyed by argument name
|
|
29
|
+
* - `rateLimit` — sliding-window counter shared per (persona.id, toolName)
|
|
30
|
+
*/
|
|
31
|
+
export interface Scope {
|
|
32
|
+
toolPattern: string | RegExp;
|
|
33
|
+
argConstraints?: Record<string, ArgRule>;
|
|
34
|
+
rateLimit?: {
|
|
35
|
+
max: number;
|
|
36
|
+
windowMs: number;
|
|
37
|
+
};
|
|
38
|
+
/** Optional radius for this scope; cannot exceed Persona's maxBlastRadius. */
|
|
39
|
+
blastRadius?: BlastRadius;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Named, composable persona. `id` is the lookup key in the registry.
|
|
43
|
+
*/
|
|
44
|
+
export interface Persona {
|
|
45
|
+
id: string;
|
|
46
|
+
description: string;
|
|
47
|
+
scopes: Scope[];
|
|
48
|
+
maxBlastRadius?: BlastRadius;
|
|
49
|
+
}
|
|
50
|
+
export interface PermissionGrant {
|
|
51
|
+
persona: Persona;
|
|
52
|
+
toolName: string;
|
|
53
|
+
args: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
export interface Verdict {
|
|
56
|
+
allowed: boolean;
|
|
57
|
+
reason?: string;
|
|
58
|
+
matchedScope?: Scope;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Thrown by enforce(). Carries the full verdict for upstream logging.
|
|
62
|
+
*/
|
|
63
|
+
export declare class PermissionDeniedError extends Error {
|
|
64
|
+
readonly verdict: Verdict;
|
|
65
|
+
readonly grant: PermissionGrant;
|
|
66
|
+
constructor(grant: PermissionGrant, verdict: Verdict);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// futures/persona/types — type-checked privilege scoping for tool invocation.
|
|
2
|
+
//
|
|
3
|
+
// A Persona is a named bundle of Scopes. A Scope binds a tool pattern to a
|
|
4
|
+
// set of argument constraints, an optional rate limit, and is bounded by
|
|
5
|
+
// the Persona's max blast radius. canInvoke() resolves a (persona, tool, args)
|
|
6
|
+
// triple to a Verdict. enforce() turns a denied Verdict into a thrown error.
|
|
7
|
+
//
|
|
8
|
+
// Module is standalone — no integration into permissions.ts yet.
|
|
9
|
+
export const BLAST_RADIUS_ORDER = [
|
|
10
|
+
'none',
|
|
11
|
+
'read-only',
|
|
12
|
+
'sandboxed',
|
|
13
|
+
'destructive',
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Thrown by enforce(). Carries the full verdict for upstream logging.
|
|
17
|
+
*/
|
|
18
|
+
export class PermissionDeniedError extends Error {
|
|
19
|
+
verdict;
|
|
20
|
+
grant;
|
|
21
|
+
constructor(grant, verdict) {
|
|
22
|
+
super(`permission denied: persona=${grant.persona.id} tool=${grant.toolName} reason=${verdict.reason ?? 'no scope matched'}`);
|
|
23
|
+
this.name = 'PermissionDeniedError';
|
|
24
|
+
this.verdict = verdict;
|
|
25
|
+
this.grant = grant;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill graph — runtime ops.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions over the SkillGraph data structure. Mutators return new
|
|
5
|
+
* instances rather than mutating in place; the underlying Maps and arrays
|
|
6
|
+
* are copied. Random walks use a seeded LCG for deterministic tests.
|
|
7
|
+
*/
|
|
8
|
+
import type { Edge, GraphPath, Scenario, Skill, SkillGraph } from './types.js';
|
|
9
|
+
export declare function buildGraph(): SkillGraph;
|
|
10
|
+
export declare function addSkill(g: SkillGraph, skill: Skill): SkillGraph;
|
|
11
|
+
export declare function addScenario(g: SkillGraph, scenario: Scenario): SkillGraph;
|
|
12
|
+
export declare function addEdge(g: SkillGraph, edge: Edge): SkillGraph;
|
|
13
|
+
export interface SampleOptions {
|
|
14
|
+
start?: string;
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
seed?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function samplePath(g: SkillGraph, opts?: SampleOptions): GraphPath;
|
|
19
|
+
export declare function findPaths(g: SkillGraph, fromId: string, toId: string, maxDepth?: number): GraphPath[];
|
|
20
|
+
export interface PathLengthStats {
|
|
21
|
+
min: number;
|
|
22
|
+
max: number;
|
|
23
|
+
avg: number;
|
|
24
|
+
p50: number;
|
|
25
|
+
p95: number;
|
|
26
|
+
samples: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function pathLengthDistribution(g: SkillGraph, samples?: number, opts?: {
|
|
29
|
+
seed?: number;
|
|
30
|
+
}): PathLengthStats;
|
|
31
|
+
//# sourceMappingURL=graph.d.ts.map
|