ai-runtime-engine 1.2.0 → 2.7.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/CHANGELOG.md +602 -0
- package/dist/agents/admit.d.ts +69 -0
- package/dist/agents/admit.js +129 -0
- package/dist/agents/definition.d.ts +36 -0
- package/dist/agents/definition.js +9 -0
- package/dist/agents/envelope.d.ts +53 -0
- package/dist/agents/envelope.js +68 -0
- package/dist/agents/finding.d.ts +79 -0
- package/dist/agents/finding.js +80 -0
- package/dist/agents/task.d.ts +60 -0
- package/dist/agents/task.js +32 -0
- package/dist/agents/worker.d.ts +68 -0
- package/dist/agents/worker.js +256 -0
- package/dist/capabilities/capability.d.ts +117 -0
- package/dist/capabilities/capability.js +66 -0
- package/dist/capabilities/registry.d.ts +139 -0
- package/dist/capabilities/registry.js +413 -0
- package/dist/capabilities/vocabulary.d.ts +32 -0
- package/dist/capabilities/vocabulary.js +34 -0
- package/dist/cli/cli.js +56 -4
- package/dist/cli/commands/cleanup.js +29 -27
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +38 -8
- package/dist/cli/commands/executions.js +34 -25
- package/dist/cli/commands/info.d.ts +1 -0
- package/dist/cli/commands/info.js +11 -9
- package/dist/cli/commands/init.js +19 -0
- package/dist/cli/commands/inspect.d.ts +40 -1
- package/dist/cli/commands/inspect.js +157 -2
- package/dist/cli/commands/mcp.d.ts +45 -0
- package/dist/cli/commands/mcp.js +148 -0
- package/dist/cli/commands/route.js +21 -0
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +36 -4
- package/dist/cli/commands/skills.d.ts +2 -0
- package/dist/cli/commands/skills.js +29 -7
- package/dist/cli/interactive/ansi.d.ts +41 -0
- package/dist/cli/interactive/ansi.js +43 -0
- package/dist/cli/interactive/complete.d.ts +10 -0
- package/dist/cli/interactive/complete.js +19 -0
- package/dist/cli/interactive/repl.d.ts +3 -0
- package/dist/cli/interactive/repl.js +105 -16
- package/dist/cli/interactive/session.d.ts +12 -1
- package/dist/cli/interactive/session.js +83 -5
- package/dist/cli/render.d.ts +13 -0
- package/dist/cli/render.js +18 -0
- package/dist/cli/runtimeSession.d.ts +11 -0
- package/dist/cli/runtimeSession.js +17 -0
- package/dist/config/defaults.d.ts +3 -1
- package/dist/config/defaults.js +2 -0
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +2 -2
- package/dist/context/lossVerifier.d.ts +24 -0
- package/dist/context/lossVerifier.js +45 -0
- package/dist/context/summarize.d.ts +19 -0
- package/dist/context/summarize.js +53 -0
- package/dist/core/fallback/fallback.d.ts +8 -0
- package/dist/core/fallback/fallback.js +3 -1
- package/dist/core/router/executor.d.ts +6 -1
- package/dist/core/router/executor.js +9 -2
- package/dist/core/router/normalize.d.ts +2 -0
- package/dist/core/router/request.js +2 -0
- package/dist/core/router/router.d.ts +3 -0
- package/dist/core/router/router.js +7 -0
- package/dist/executions/execution.d.ts +13 -2
- package/dist/generation/generateAdapter.d.ts +14 -0
- package/dist/generation/generateAdapter.js +38 -0
- package/dist/generation/generateSkill.d.ts +26 -0
- package/dist/generation/generateSkill.js +51 -0
- package/dist/index.d.ts +44 -5
- package/dist/index.js +26 -2
- package/dist/mcp/client.d.ts +70 -0
- package/dist/mcp/client.js +221 -0
- package/dist/mcp/manager.d.ts +151 -0
- package/dist/mcp/manager.js +493 -0
- package/dist/mcp/protocol.d.ts +216 -0
- package/dist/mcp/protocol.js +149 -0
- package/dist/mcp/toolAdapter.d.ts +44 -0
- package/dist/mcp/toolAdapter.js +94 -0
- package/dist/mcp/transport.d.ts +109 -0
- package/dist/mcp/transport.js +383 -0
- package/dist/memory/embedders/hash.d.ts +12 -0
- package/dist/memory/embedders/hash.js +31 -0
- package/dist/memory/embedders/http.d.ts +25 -0
- package/dist/memory/embedders/http.js +48 -0
- package/dist/memory/memory.d.ts +19 -2
- package/dist/memory/memory.js +75 -11
- package/dist/memory/semantic.d.ts +17 -0
- package/dist/memory/semantic.js +29 -0
- package/dist/orchestration/budget.d.ts +30 -0
- package/dist/orchestration/budget.js +40 -0
- package/dist/orchestration/executor.d.ts +39 -1
- package/dist/orchestration/executor.js +64 -4
- package/dist/orchestration/orchestrator.d.ts +29 -1
- package/dist/orchestration/orchestrator.js +89 -8
- package/dist/orchestration/plan.d.ts +15 -1
- package/dist/orchestration/plan.js +23 -4
- package/dist/orchestration/planner.d.ts +19 -1
- package/dist/orchestration/planner.js +25 -5
- package/dist/plugin/ai.d.ts +4 -0
- package/dist/plugin/ai.js +9 -0
- package/dist/providers/httpClient.d.ts +25 -1
- package/dist/providers/httpClient.js +93 -0
- package/dist/providers/httpProvider.d.ts +1 -0
- package/dist/providers/httpProvider.js +67 -1
- package/dist/providers/mock/mockProvider.d.ts +3 -0
- package/dist/providers/mock/mockProvider.js +54 -0
- package/dist/providers/mock/scenarios.d.ts +7 -0
- package/dist/providers/provider.d.ts +6 -0
- package/dist/providers/wire/anthropicWire.js +34 -0
- package/dist/providers/wire/openaiWire.js +30 -0
- package/dist/providers/wire/types.d.ts +16 -0
- package/dist/runtime/config.js +50 -6
- package/dist/runtime/intent/aiClassifier.d.ts +19 -0
- package/dist/runtime/intent/aiClassifier.js +74 -0
- package/dist/runtime/models/modelProfile.d.ts +61 -0
- package/dist/runtime/models/modelProfile.js +139 -0
- package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
- package/dist/runtime/planning/deriveCapabilities.js +146 -0
- package/dist/runtime/policy.d.ts +10 -0
- package/dist/runtime/policy.js +9 -2
- package/dist/runtime/runtime.d.ts +173 -0
- package/dist/runtime/runtime.js +723 -50
- package/dist/runtime/types.d.ts +94 -2
- package/dist/skills/manifest.d.ts +3 -0
- package/dist/skills/manifest.js +24 -0
- package/dist/skills/registry.d.ts +16 -1
- package/dist/skills/registry.js +21 -1
- package/dist/skills/skill.d.ts +6 -1
- package/dist/store/area.d.ts +15 -1
- package/dist/store/area.js +19 -8
- package/dist/store/crypto.d.ts +21 -0
- package/dist/store/crypto.js +49 -0
- package/dist/store/paths.d.ts +5 -1
- package/dist/store/paths.js +6 -0
- package/dist/store/store.d.ts +15 -3
- package/dist/store/store.js +28 -7
- package/dist/telemetry/sinks/otlp.d.ts +31 -0
- package/dist/telemetry/sinks/otlp.js +76 -0
- package/dist/tools/builtins/filesystem.js +1 -0
- package/dist/tools/builtins/git.js +1 -0
- package/dist/tools/builtins/shell.js +1 -0
- package/dist/tools/permissions.d.ts +28 -0
- package/dist/tools/permissions.js +72 -0
- package/dist/tools/registry.d.ts +18 -2
- package/dist/tools/registry.js +22 -2
- package/dist/tools/tool.d.ts +4 -0
- package/dist/types.d.ts +11 -1
- package/dist/util/flatten.d.ts +11 -0
- package/dist/util/flatten.js +18 -0
- package/dist/util/semaphore.d.ts +19 -0
- package/dist/util/semaphore.js +60 -0
- package/package.json +24 -9
package/dist/store/store.js
CHANGED
|
@@ -7,17 +7,21 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync }
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { systemClock } from '../util/clock.js';
|
|
9
9
|
import { FileArea, NullArea } from './area.js';
|
|
10
|
-
import { projectId as computeProjectId, repositoryId as computeRepositoryId, resolveHome } from './paths.js';
|
|
10
|
+
import { projectId as computeProjectId, repositoryId as computeRepositoryId, organizationId as computeOrganizationId, resolveHome } from './paths.js';
|
|
11
11
|
export const STORE_VERSION = 1;
|
|
12
12
|
export class RuntimeStore {
|
|
13
13
|
enabled;
|
|
14
14
|
projectId;
|
|
15
15
|
repositoryId;
|
|
16
|
+
/** Present only when an organization is configured — the availability signal for the `organization` scope. */
|
|
17
|
+
organizationId;
|
|
16
18
|
clock;
|
|
17
19
|
home;
|
|
18
20
|
projectDir;
|
|
19
21
|
repoDir;
|
|
20
22
|
userDir;
|
|
23
|
+
orgDir;
|
|
24
|
+
cipher;
|
|
21
25
|
areas = new Map();
|
|
22
26
|
metaStamped = false;
|
|
23
27
|
constructor(options) {
|
|
@@ -30,26 +34,38 @@ export class RuntimeStore {
|
|
|
30
34
|
this.projectDir = join(this.home, 'projects', this.projectId);
|
|
31
35
|
this.repoDir = join(this.home, 'repos', this.repositoryId);
|
|
32
36
|
this.userDir = join(this.home, 'user');
|
|
37
|
+
if (options.organization && options.organization.trim()) {
|
|
38
|
+
this.organizationId = computeOrganizationId(options.organization);
|
|
39
|
+
this.orgDir = join(this.home, 'organizations', this.organizationId);
|
|
40
|
+
}
|
|
41
|
+
if (options.cipher)
|
|
42
|
+
this.cipher = options.cipher;
|
|
33
43
|
}
|
|
34
44
|
paths() {
|
|
35
|
-
return { home: this.home, project: this.projectDir, repository: this.repoDir, user: this.userDir };
|
|
45
|
+
return { home: this.home, project: this.projectDir, repository: this.repoDir, user: this.userDir, ...(this.orgDir ? { organization: this.orgDir } : {}) };
|
|
36
46
|
}
|
|
37
47
|
area(dir) {
|
|
38
48
|
if (!this.enabled)
|
|
39
|
-
return new NullArea();
|
|
49
|
+
return new NullArea(); // stateless areas are never wrapped
|
|
40
50
|
let a = this.areas.get(dir);
|
|
41
51
|
if (!a) {
|
|
42
|
-
a = new FileArea(dir);
|
|
52
|
+
a = new FileArea(dir, this.cipher); // cipher (when configured) encrypts every file area transparently
|
|
43
53
|
this.areas.set(dir, a);
|
|
44
54
|
}
|
|
45
55
|
return a;
|
|
46
56
|
}
|
|
47
|
-
/** Memory area for a physical scope. */
|
|
57
|
+
/** Memory area for a physical scope. `organization` requires a configured org (guarded by callers). */
|
|
48
58
|
memory(scope) {
|
|
49
59
|
if (scope === 'user')
|
|
50
60
|
return this.area(join(this.userDir, 'memory'));
|
|
51
61
|
if (scope === 'repository')
|
|
52
62
|
return this.area(join(this.repoDir, 'memory'));
|
|
63
|
+
if (scope === 'organization') {
|
|
64
|
+
// Callers only traverse this scope when `organizationId` is set; a stray call with no org is a no-op.
|
|
65
|
+
if (!this.orgDir)
|
|
66
|
+
return new NullArea();
|
|
67
|
+
return this.area(join(this.orgDir, 'memory'));
|
|
68
|
+
}
|
|
53
69
|
return this.area(join(this.projectDir, 'memory'));
|
|
54
70
|
}
|
|
55
71
|
conversations() {
|
|
@@ -58,6 +74,10 @@ export class RuntimeStore {
|
|
|
58
74
|
indexes() {
|
|
59
75
|
return this.area(join(this.projectDir, 'indexes'));
|
|
60
76
|
}
|
|
77
|
+
/** MCP state: dynamic servers, enable/disable overrides, and per-server tool caches (Phase 3.2). */
|
|
78
|
+
mcp() {
|
|
79
|
+
return this.area(join(this.projectDir, 'mcp'));
|
|
80
|
+
}
|
|
61
81
|
cache() {
|
|
62
82
|
return this.area(join(this.projectDir, 'cache'));
|
|
63
83
|
}
|
|
@@ -131,10 +151,11 @@ export class RuntimeStore {
|
|
|
131
151
|
rmSync(lockPath);
|
|
132
152
|
}
|
|
133
153
|
}
|
|
134
|
-
/** Aggregate integrity check across the project areas. */
|
|
154
|
+
/** Aggregate integrity check across the project areas (plus the org area when configured). */
|
|
135
155
|
check() {
|
|
136
156
|
if (!this.enabled)
|
|
137
157
|
return [];
|
|
138
|
-
|
|
158
|
+
const org = this.orgDir ? this.memory('organization').check() : [];
|
|
159
|
+
return [...this.memory('project').check(), ...this.memory('user').check(), ...this.memory('repository').check(), ...org, ...this.conversations().check(), ...this.indexes().check()];
|
|
139
160
|
}
|
|
140
161
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTLP-shaped telemetry exporter (Phase 19). Maps the four frozen `TelemetryEvent` members to an
|
|
3
|
+
* OpenTelemetry-JSON-ish payload (resource + a scope with logRecords) and POSTs them, batched, to an
|
|
4
|
+
* OTLP/HTTP-style `endpoint`. Metadata ONLY — the events are already routing/perf metadata and are passed
|
|
5
|
+
* through `redact()` before they leave the process; prompt text / inputs / keys never appear. Like every
|
|
6
|
+
* sink, ALL IO is wrapped and swallowed: telemetry must never fail a run.
|
|
7
|
+
*
|
|
8
|
+
* An optional auth header value is read from the env var NAMED by `headersEnv` (never a value in config).
|
|
9
|
+
*/
|
|
10
|
+
import type { TelemetrySink } from '../telemetry.js';
|
|
11
|
+
import type { TelemetryEvent } from '../../types.js';
|
|
12
|
+
import type { FetchLike } from '../../providers/httpClient.js';
|
|
13
|
+
export interface OtlpSinkOptions {
|
|
14
|
+
endpoint: string;
|
|
15
|
+
fetchImpl?: FetchLike;
|
|
16
|
+
/** Resolved auth header value (from an env-var NAME upstream), sent as `authorization` if present. */
|
|
17
|
+
authHeader?: string;
|
|
18
|
+
/** Flush when this many events are buffered (default 20). */
|
|
19
|
+
batchSize?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class OtlpSink implements TelemetrySink {
|
|
22
|
+
private readonly endpoint;
|
|
23
|
+
private readonly fetchImpl;
|
|
24
|
+
private readonly authHeader?;
|
|
25
|
+
private readonly batchSize;
|
|
26
|
+
private buffer;
|
|
27
|
+
constructor(opts: OtlpSinkOptions);
|
|
28
|
+
emit(event: TelemetryEvent): void;
|
|
29
|
+
/** POST the buffered batch. Fire-and-forget; every error is swallowed. */
|
|
30
|
+
flush(): void;
|
|
31
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTLP-shaped telemetry exporter (Phase 19). Maps the four frozen `TelemetryEvent` members to an
|
|
3
|
+
* OpenTelemetry-JSON-ish payload (resource + a scope with logRecords) and POSTs them, batched, to an
|
|
4
|
+
* OTLP/HTTP-style `endpoint`. Metadata ONLY — the events are already routing/perf metadata and are passed
|
|
5
|
+
* through `redact()` before they leave the process; prompt text / inputs / keys never appear. Like every
|
|
6
|
+
* sink, ALL IO is wrapped and swallowed: telemetry must never fail a run.
|
|
7
|
+
*
|
|
8
|
+
* An optional auth header value is read from the env var NAMED by `headersEnv` (never a value in config).
|
|
9
|
+
*/
|
|
10
|
+
import { redact } from '../../security/redact.js';
|
|
11
|
+
/** Severity mapping for the four event types (INFO by default; a failed result is WARN). */
|
|
12
|
+
function toLogRecord(event) {
|
|
13
|
+
const attributes = Object.entries(event)
|
|
14
|
+
.filter(([k]) => k !== 'ts' && k !== 'type')
|
|
15
|
+
.map(([k, v]) => ({ key: k, value: otlpAnyValue(v) }));
|
|
16
|
+
const severe = event.type === 'route.result' && event.ok === false;
|
|
17
|
+
return {
|
|
18
|
+
timeUnixNano: String(BigInt(Math.trunc(event.ts)) * 1000000n), // ms → ns
|
|
19
|
+
severityText: severe ? 'WARN' : 'INFO',
|
|
20
|
+
body: { stringValue: event.type },
|
|
21
|
+
attributes,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function otlpAnyValue(v) {
|
|
25
|
+
if (typeof v === 'string')
|
|
26
|
+
return { stringValue: v };
|
|
27
|
+
if (typeof v === 'boolean')
|
|
28
|
+
return { boolValue: v };
|
|
29
|
+
if (typeof v === 'number')
|
|
30
|
+
return Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
|
|
31
|
+
return { stringValue: JSON.stringify(v) };
|
|
32
|
+
}
|
|
33
|
+
export class OtlpSink {
|
|
34
|
+
endpoint;
|
|
35
|
+
fetchImpl;
|
|
36
|
+
authHeader;
|
|
37
|
+
batchSize;
|
|
38
|
+
buffer = [];
|
|
39
|
+
constructor(opts) {
|
|
40
|
+
this.endpoint = opts.endpoint;
|
|
41
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
42
|
+
if (opts.authHeader)
|
|
43
|
+
this.authHeader = opts.authHeader;
|
|
44
|
+
this.batchSize = opts.batchSize && opts.batchSize > 0 ? opts.batchSize : 20;
|
|
45
|
+
}
|
|
46
|
+
emit(event) {
|
|
47
|
+
this.buffer.push(redact(event));
|
|
48
|
+
if (this.buffer.length >= this.batchSize)
|
|
49
|
+
this.flush();
|
|
50
|
+
}
|
|
51
|
+
/** POST the buffered batch. Fire-and-forget; every error is swallowed. */
|
|
52
|
+
flush() {
|
|
53
|
+
if (this.buffer.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
const batch = this.buffer;
|
|
56
|
+
this.buffer = [];
|
|
57
|
+
const payload = {
|
|
58
|
+
resourceLogs: [
|
|
59
|
+
{
|
|
60
|
+
resource: { attributes: [{ key: 'service.name', value: { stringValue: 'ai-runtime' } }] },
|
|
61
|
+
scopeLogs: [{ scope: { name: 'ai-runtime.router' }, logRecords: batch.map(toLogRecord) }],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
const headers = { 'content-type': 'application/json' };
|
|
66
|
+
if (this.authHeader)
|
|
67
|
+
headers.authorization = this.authHeader;
|
|
68
|
+
try {
|
|
69
|
+
// Fire-and-forget: never await, never let a rejection escape (telemetry must not fail a run).
|
|
70
|
+
void Promise.resolve(this.fetchImpl(this.endpoint, { method: 'POST', headers, body: JSON.stringify(payload) })).catch(() => { });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* a telemetry export must never fail a run */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -10,6 +10,7 @@ import { resolveInJail, JailError } from '../jail.js';
|
|
|
10
10
|
export const filesystemTool = {
|
|
11
11
|
id: 'filesystem',
|
|
12
12
|
description: 'Read, write, list, and check files within the workspace (jailed; no escape).',
|
|
13
|
+
provides: [{ id: 'read_file' }, { id: 'write_file' }],
|
|
13
14
|
parameters: { op: 'read | write | list | exists', path: 'string (relative to workspace)', content: 'string (write only)' },
|
|
14
15
|
async execute(input, ctx) {
|
|
15
16
|
const { op, path, content } = (input ?? {});
|
|
@@ -19,6 +19,7 @@ export function createGitTool(runner = defaultRunner) {
|
|
|
19
19
|
id: 'git',
|
|
20
20
|
description: 'Read git state (status/diff/log); commit/push are permission-gated and off by default.',
|
|
21
21
|
parameters: { op: 'status | diff | log | commit | push', message: 'string (commit)', args: 'string[] (refs only)' },
|
|
22
|
+
provides: [{ id: 'read_repository' }, { id: 'commit_changes' }],
|
|
22
23
|
async execute(input, ctx) {
|
|
23
24
|
const { op, message, args = [] } = (input ?? {});
|
|
24
25
|
if (!op)
|
|
@@ -56,6 +56,7 @@ export function createShellTool(runner = defaultRunner) {
|
|
|
56
56
|
id: 'shell',
|
|
57
57
|
description: 'Run an allowlisted shell command in the workspace (destructive commands need approval).',
|
|
58
58
|
parameters: { command: 'string (argv[0])', args: 'string[]' },
|
|
59
|
+
provides: [{ id: 'run_command' }, { id: 'run_tests' }],
|
|
59
60
|
async execute(input, ctx) {
|
|
60
61
|
const { command, args = [] } = (input ?? {});
|
|
61
62
|
if (typeof command !== 'string' || command === '')
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* allowlist fields win over the flat Phase-1 fields when both are present.
|
|
6
6
|
*/
|
|
7
7
|
import type { PermissionPolicy } from '../runtime/policy.js';
|
|
8
|
+
/** Per-server MCP grant. `off` denies before any prompt; `read` admits read-only tools; `full` admits all. */
|
|
9
|
+
export type McpServerGrant = 'off' | 'read' | 'full';
|
|
8
10
|
export interface ResolvedPermissions {
|
|
9
11
|
filesystem: {
|
|
10
12
|
read: boolean;
|
|
@@ -21,5 +23,31 @@ export interface ResolvedPermissions {
|
|
|
21
23
|
network: {
|
|
22
24
|
enabled: boolean;
|
|
23
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* MCP execution (Phase 3.2). This is the SOLE authority for MCP tool execution: adding or connecting a
|
|
28
|
+
* server grants nothing, and `network`/`shell` are tool-local concerns that do not gate MCP transports
|
|
29
|
+
* (recorded as a decision — one dimension answers one question, so deny-by-default cannot fail open).
|
|
30
|
+
*/
|
|
31
|
+
mcp: {
|
|
32
|
+
servers: Record<string, McpServerGrant>;
|
|
33
|
+
};
|
|
24
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Combine two MCP permission maps by MINIMUM per server — the only nested permission dimension, so its
|
|
37
|
+
* merge rule is explicit: an override can never widen a grant, and a server absent from the override
|
|
38
|
+
* keeps the base grant. Every agent/skill/tool override path must go through this.
|
|
39
|
+
*/
|
|
40
|
+
export declare function clampMcpPermissions(base: Record<string, McpServerGrant>, override: Record<string, McpServerGrant | boolean> | undefined): Record<string, McpServerGrant>;
|
|
41
|
+
/**
|
|
42
|
+
* Combine two permission policies by MINIMUM — the permission half of an agent envelope.
|
|
43
|
+
*
|
|
44
|
+
* The output is FULLY EXPLICIT on every dimension (it is resolved through `resolvePermissions` first),
|
|
45
|
+
* and that is what makes it safe to hand to `Runtime.resolvedPermissions` as an override: that merge is
|
|
46
|
+
* a shallow spread, so an override with ABSENT fields would silently fall back to defaults — and
|
|
47
|
+
* `fsRead` defaults to TRUE. Emitting explicit values means the spread can only ever narrow.
|
|
48
|
+
*
|
|
49
|
+
* Booleans combine by AND, `shellAllowedCommands` by INTERSECTION, and `mcp.servers` by
|
|
50
|
+
* `clampMcpPermissions` (per-server minimum). An override can never widen anything.
|
|
51
|
+
*/
|
|
52
|
+
export declare function clampPermissions(base: PermissionPolicy, override?: PermissionPolicy): PermissionPolicy;
|
|
25
53
|
export declare function resolvePermissions(policy?: PermissionPolicy): ResolvedPermissions;
|
|
@@ -4,6 +4,74 @@
|
|
|
4
4
|
* the workspace), writes/shell/git-mutations/network are OFF unless explicitly granted. The structured
|
|
5
5
|
* allowlist fields win over the flat Phase-1 fields when both are present.
|
|
6
6
|
*/
|
|
7
|
+
const GRANT_RANK = { off: 0, read: 1, full: 2 };
|
|
8
|
+
/**
|
|
9
|
+
* Normalize the config-facing value (`true` ≡ full, `false` ≡ off) into a grant. Anything that is not
|
|
10
|
+
* exactly one of the three grant names collapses to `off`: the value comes from a YAML file, so
|
|
11
|
+
* `read: yes` (a string), a number, or a typo must never be treated as "some grant".
|
|
12
|
+
*/
|
|
13
|
+
function toGrant(v) {
|
|
14
|
+
if (v === true)
|
|
15
|
+
return 'full';
|
|
16
|
+
if (v === 'off' || v === 'read' || v === 'full')
|
|
17
|
+
return v;
|
|
18
|
+
return 'off';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Combine two MCP permission maps by MINIMUM per server — the only nested permission dimension, so its
|
|
22
|
+
* merge rule is explicit: an override can never widen a grant, and a server absent from the override
|
|
23
|
+
* keeps the base grant. Every agent/skill/tool override path must go through this.
|
|
24
|
+
*/
|
|
25
|
+
export function clampMcpPermissions(base, override) {
|
|
26
|
+
const out = Object.create(null);
|
|
27
|
+
for (const [k, v] of Object.entries(base))
|
|
28
|
+
out[k] = toGrant(v);
|
|
29
|
+
if (!override)
|
|
30
|
+
return out;
|
|
31
|
+
for (const [server, raw] of Object.entries(override)) {
|
|
32
|
+
const want = toGrant(raw);
|
|
33
|
+
const have = Object.prototype.hasOwnProperty.call(out, server) ? out[server] : 'off';
|
|
34
|
+
out[server] = GRANT_RANK[want] < GRANT_RANK[have] ? want : have;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Build a prototype-free grant map, so an unconfigured key can only ever answer `undefined`. */
|
|
39
|
+
function grantMap(raw) {
|
|
40
|
+
const out = Object.create(null);
|
|
41
|
+
for (const [k, v] of Object.entries(raw ?? {}))
|
|
42
|
+
out[k] = toGrant(v);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Combine two permission policies by MINIMUM — the permission half of an agent envelope.
|
|
47
|
+
*
|
|
48
|
+
* The output is FULLY EXPLICIT on every dimension (it is resolved through `resolvePermissions` first),
|
|
49
|
+
* and that is what makes it safe to hand to `Runtime.resolvedPermissions` as an override: that merge is
|
|
50
|
+
* a shallow spread, so an override with ABSENT fields would silently fall back to defaults — and
|
|
51
|
+
* `fsRead` defaults to TRUE. Emitting explicit values means the spread can only ever narrow.
|
|
52
|
+
*
|
|
53
|
+
* Booleans combine by AND, `shellAllowedCommands` by INTERSECTION, and `mcp.servers` by
|
|
54
|
+
* `clampMcpPermissions` (per-server minimum). An override can never widen anything.
|
|
55
|
+
*/
|
|
56
|
+
export function clampPermissions(base, override) {
|
|
57
|
+
const b = resolvePermissions(base);
|
|
58
|
+
const and = (left, right) => left && (right ?? true);
|
|
59
|
+
const allowed = override?.shellAllowedCommands
|
|
60
|
+
? b.shell.allowedCommands.filter((c) => override.shellAllowedCommands.includes(c))
|
|
61
|
+
: [...b.shell.allowedCommands];
|
|
62
|
+
return {
|
|
63
|
+
fsRead: and(b.filesystem.read, override?.fsRead),
|
|
64
|
+
fsWrite: and(b.filesystem.write, override?.fsWrite),
|
|
65
|
+
shell: and(b.shell.enabled, override?.shell),
|
|
66
|
+
shellAllowedCommands: allowed,
|
|
67
|
+
// `gitWrite` is the Phase-1 umbrella; emitting the two explicit grants instead means the resolved
|
|
68
|
+
// view cannot be widened by an absent umbrella on a later merge.
|
|
69
|
+
gitCommit: and(b.git.commit, override?.gitCommit ?? override?.gitWrite),
|
|
70
|
+
gitPush: and(b.git.push, override?.gitPush ?? override?.gitWrite),
|
|
71
|
+
network: and(b.network.enabled, override?.network),
|
|
72
|
+
mcp: { servers: { ...clampMcpPermissions(b.mcp.servers, override?.mcp?.servers) } },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
7
75
|
export function resolvePermissions(policy = {}) {
|
|
8
76
|
const gitDefault = policy.gitWrite ?? false;
|
|
9
77
|
return {
|
|
@@ -20,5 +88,9 @@ export function resolvePermissions(policy = {}) {
|
|
|
20
88
|
push: policy.gitPush ?? gitDefault,
|
|
21
89
|
},
|
|
22
90
|
network: { enabled: policy.network ?? false },
|
|
91
|
+
// A NULL-PROTOTYPE map: `Object.fromEntries` inherits Object.prototype, so `servers['constructor']`
|
|
92
|
+
// would answer with a function — and a lookup of `?? 'off'` would then see a truthy non-'off' value.
|
|
93
|
+
// Deny-by-default has to hold for every possible key, not just the configured ones.
|
|
94
|
+
mcp: { servers: grantMap(policy.mcp?.servers) },
|
|
23
95
|
};
|
|
24
96
|
}
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* A registry of tools, keyed by id. Registration is idempotent-by-id (last wins, like tasks).
|
|
3
|
+
*
|
|
4
|
+
* An optional change listener (Phase 3.1) lets the Runtime ingest ACTION-capability declarations at the
|
|
5
|
+
* one real registration choke point — builtins, user `registerTool`, and future sources all pass through
|
|
6
|
+
* `register()`. The zero-arg constructor is preserved (this class is public API).
|
|
7
|
+
*/
|
|
2
8
|
import type { Tool } from './tool.js';
|
|
9
|
+
export interface ToolRegistryListener {
|
|
10
|
+
/** Called after a tool is registered. `trusted` marks in-tree builtins (see the claiming rule). */
|
|
11
|
+
onRegister?: (tool: Tool, trusted: boolean) => void;
|
|
12
|
+
onUnregister?: (id: string) => void;
|
|
13
|
+
}
|
|
3
14
|
export declare class ToolRegistry {
|
|
4
15
|
private readonly tools;
|
|
5
|
-
|
|
16
|
+
private listener;
|
|
17
|
+
/** Attach the change listener (called once by the Runtime before builtin registration). */
|
|
18
|
+
attach(listener: ToolRegistryListener): this;
|
|
19
|
+
/** Register a tool. `trusted` is set only for in-tree builtins registered by the Runtime itself. */
|
|
20
|
+
register(tool: Tool, trusted?: boolean): this;
|
|
21
|
+
unregister(id: string): boolean;
|
|
6
22
|
get(id: string): Tool | undefined;
|
|
7
23
|
has(id: string): boolean;
|
|
8
24
|
list(): Tool[];
|
package/dist/tools/registry.js
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* A registry of tools, keyed by id. Registration is idempotent-by-id (last wins, like tasks).
|
|
3
|
+
*
|
|
4
|
+
* An optional change listener (Phase 3.1) lets the Runtime ingest ACTION-capability declarations at the
|
|
5
|
+
* one real registration choke point — builtins, user `registerTool`, and future sources all pass through
|
|
6
|
+
* `register()`. The zero-arg constructor is preserved (this class is public API).
|
|
7
|
+
*/
|
|
2
8
|
export class ToolRegistry {
|
|
3
9
|
tools = new Map();
|
|
4
|
-
|
|
10
|
+
listener = {};
|
|
11
|
+
/** Attach the change listener (called once by the Runtime before builtin registration). */
|
|
12
|
+
attach(listener) {
|
|
13
|
+
this.listener = listener ?? {};
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
/** Register a tool. `trusted` is set only for in-tree builtins registered by the Runtime itself. */
|
|
17
|
+
register(tool, trusted = false) {
|
|
5
18
|
this.tools.set(tool.id, tool);
|
|
19
|
+
this.listener.onRegister?.(tool, trusted);
|
|
6
20
|
return this;
|
|
7
21
|
}
|
|
22
|
+
unregister(id) {
|
|
23
|
+
const had = this.tools.delete(id);
|
|
24
|
+
if (had)
|
|
25
|
+
this.listener.onUnregister?.(id);
|
|
26
|
+
return had;
|
|
27
|
+
}
|
|
8
28
|
get(id) {
|
|
9
29
|
return this.tools.get(id);
|
|
10
30
|
}
|
package/dist/tools/tool.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { ArtifactRef } from '../runtime/types.js';
|
|
|
9
9
|
import type { ApprovalProvider } from '../runtime/host.js';
|
|
10
10
|
import type { Clock } from '../util/clock.js';
|
|
11
11
|
import type { ResolvedPermissions } from './permissions.js';
|
|
12
|
+
import type { ActionCapabilityDecl } from '../capabilities/capability.js';
|
|
12
13
|
export interface ToolContext {
|
|
13
14
|
workspaceRoot: string;
|
|
14
15
|
permissions: ResolvedPermissions;
|
|
@@ -35,6 +36,9 @@ export interface Tool {
|
|
|
35
36
|
description: string;
|
|
36
37
|
/** JSON-schema-ish parameter description (advisory; validated by the tool's own execute). */
|
|
37
38
|
parameters?: unknown;
|
|
39
|
+
/** ACTION capabilities this tool provides (Phase 3.1). Advisory metadata, NEVER authorization — this
|
|
40
|
+
* tool's own permission checks remain the only gate. Absent ⇒ one inferred `tool:<id>` entry. */
|
|
41
|
+
provides?: ActionCapabilityDecl[];
|
|
38
42
|
execute(input: unknown, ctx: ToolContext): Promise<ToolResult>;
|
|
39
43
|
}
|
|
40
44
|
/** Helper: a denied result with a consistent shape. */
|
package/dist/types.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface AIRequest {
|
|
|
80
80
|
signal?: AbortSignal;
|
|
81
81
|
sensitivity: Sensitivity;
|
|
82
82
|
metadata?: Record<string, unknown>;
|
|
83
|
+
/** Request token-by-token streaming (text output only). A provider without `executeStream` ignores it. */
|
|
84
|
+
stream?: boolean;
|
|
83
85
|
}
|
|
84
86
|
export type FinishReason = 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error';
|
|
85
87
|
/** What every provider adapter returns — normalized so callers never parse vendor JSON. */
|
|
@@ -335,6 +337,10 @@ export interface RunRequest {
|
|
|
335
337
|
verification?: boolean;
|
|
336
338
|
/** Attach tools from registered MCP sources to this run (requires a tool-calling model). */
|
|
337
339
|
mcp?: boolean;
|
|
340
|
+
/** Stream the answer token-by-token (text output only; ignored for JSON output). Needs `onDelta`. */
|
|
341
|
+
stream?: boolean;
|
|
342
|
+
/** Called with each text chunk as it streams. The final `AIResponse.text` is still the full aggregate. */
|
|
343
|
+
onDelta?: (chunk: string) => void;
|
|
338
344
|
}
|
|
339
345
|
/**
|
|
340
346
|
* User exclude/prefer routing (all optional). EXCLUDE is a HARD filter — an excluded candidate is never
|
|
@@ -375,9 +381,13 @@ export interface ProviderConfig {
|
|
|
375
381
|
}
|
|
376
382
|
export interface TelemetryConfig {
|
|
377
383
|
enabled: boolean;
|
|
378
|
-
sink: 'memory' | 'file';
|
|
384
|
+
sink: 'memory' | 'file' | 'otlp';
|
|
379
385
|
storePrompts: false;
|
|
380
386
|
path?: string;
|
|
387
|
+
/** OTLP sink: the OTLP/HTTP endpoint to POST batched, redacted, metadata-only events to. */
|
|
388
|
+
endpoint?: string;
|
|
389
|
+
/** OTLP sink: env-var NAME (never a value) whose value is sent as the `authorization` header. */
|
|
390
|
+
headersEnv?: string;
|
|
381
391
|
}
|
|
382
392
|
export interface RouterConfig {
|
|
383
393
|
providers: ProviderConfig[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flatten and clamp a source-, server- or MODEL-controlled string before it crosses a rendering
|
|
3
|
+
* boundary. ONE implementation behind three named entry points: `promptSafe` (model prompts,
|
|
4
|
+
* src/runtime/runtime.ts), `displaySafe` (the terminal, src/cli/render.ts), and the capability deriver's
|
|
5
|
+
* candidate labels. Internal — deliberately not exported from src/index.ts.
|
|
6
|
+
*
|
|
7
|
+
* The two properties that matter: no control characters or newlines survive (so a value can never forge
|
|
8
|
+
* an extra row in a rendered list), and the length is bounded (so it cannot dominate a prompt or a
|
|
9
|
+
* terminal line).
|
|
10
|
+
*/
|
|
11
|
+
export declare function flattenClamp(raw: string, max?: number): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flatten and clamp a source-, server- or MODEL-controlled string before it crosses a rendering
|
|
3
|
+
* boundary. ONE implementation behind three named entry points: `promptSafe` (model prompts,
|
|
4
|
+
* src/runtime/runtime.ts), `displaySafe` (the terminal, src/cli/render.ts), and the capability deriver's
|
|
5
|
+
* candidate labels. Internal — deliberately not exported from src/index.ts.
|
|
6
|
+
*
|
|
7
|
+
* The two properties that matter: no control characters or newlines survive (so a value can never forge
|
|
8
|
+
* an extra row in a rendered list), and the length is bounded (so it cannot dominate a prompt or a
|
|
9
|
+
* terminal line).
|
|
10
|
+
*/
|
|
11
|
+
export function flattenClamp(raw, max = 80) {
|
|
12
|
+
const flat = String(raw ?? '')
|
|
13
|
+
// eslint-disable-next-line no-control-regex
|
|
14
|
+
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
15
|
+
.replace(/\s+/g, ' ')
|
|
16
|
+
.trim();
|
|
17
|
+
return flat.length > max ? `${flat.slice(0, max)}…` : flat;
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny keyed async semaphore (Phase 19). Each key has an independent concurrency limit; `run(key, fn)`
|
|
3
|
+
* waits until a slot for that key is free, runs `fn`, and releases the slot even if `fn` throws. Waiters are
|
|
4
|
+
* served FIFO. A key with no configured limit (or a limit ≤ 0) is UNLIMITED — `run` calls `fn` immediately,
|
|
5
|
+
* so an empty limits map is a zero-overhead pass-through (the "no concurrency config" default).
|
|
6
|
+
*/
|
|
7
|
+
export declare class KeyedSemaphore {
|
|
8
|
+
private readonly limits;
|
|
9
|
+
private readonly inFlight;
|
|
10
|
+
private readonly waiters;
|
|
11
|
+
constructor(limits?: Record<string, number>);
|
|
12
|
+
/** Whether this semaphore constrains anything at all (empty/limitless → no-op fast path). */
|
|
13
|
+
get active(): boolean;
|
|
14
|
+
private limitFor;
|
|
15
|
+
acquire(key: string): Promise<void>;
|
|
16
|
+
release(key: string): void;
|
|
17
|
+
/** Acquire a slot for `key`, run `fn`, and always release — even on throw. */
|
|
18
|
+
run<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny keyed async semaphore (Phase 19). Each key has an independent concurrency limit; `run(key, fn)`
|
|
3
|
+
* waits until a slot for that key is free, runs `fn`, and releases the slot even if `fn` throws. Waiters are
|
|
4
|
+
* served FIFO. A key with no configured limit (or a limit ≤ 0) is UNLIMITED — `run` calls `fn` immediately,
|
|
5
|
+
* so an empty limits map is a zero-overhead pass-through (the "no concurrency config" default).
|
|
6
|
+
*/
|
|
7
|
+
export class KeyedSemaphore {
|
|
8
|
+
limits;
|
|
9
|
+
inFlight = new Map();
|
|
10
|
+
waiters = new Map();
|
|
11
|
+
constructor(limits = {}) {
|
|
12
|
+
this.limits = limits;
|
|
13
|
+
}
|
|
14
|
+
/** Whether this semaphore constrains anything at all (empty/limitless → no-op fast path). */
|
|
15
|
+
get active() {
|
|
16
|
+
return Object.values(this.limits).some((n) => n > 0);
|
|
17
|
+
}
|
|
18
|
+
limitFor(key) {
|
|
19
|
+
const n = this.limits[key];
|
|
20
|
+
return n !== undefined && n > 0 ? n : undefined;
|
|
21
|
+
}
|
|
22
|
+
async acquire(key) {
|
|
23
|
+
const limit = this.limitFor(key);
|
|
24
|
+
if (limit === undefined)
|
|
25
|
+
return; // unlimited
|
|
26
|
+
const cur = this.inFlight.get(key) ?? 0;
|
|
27
|
+
if (cur < limit) {
|
|
28
|
+
this.inFlight.set(key, cur + 1);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await new Promise((resolve) => {
|
|
32
|
+
const q = this.waiters.get(key) ?? [];
|
|
33
|
+
q.push(resolve);
|
|
34
|
+
this.waiters.set(key, q);
|
|
35
|
+
});
|
|
36
|
+
// Resumed by release(), which handed its slot directly to us — the in-flight count is unchanged.
|
|
37
|
+
}
|
|
38
|
+
release(key) {
|
|
39
|
+
const limit = this.limitFor(key);
|
|
40
|
+
if (limit === undefined)
|
|
41
|
+
return; // unlimited — nothing was counted
|
|
42
|
+
const q = this.waiters.get(key);
|
|
43
|
+
if (q && q.length) {
|
|
44
|
+
const next = q.shift();
|
|
45
|
+
next(); // hand the slot to the next waiter; count stays at the limit
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.inFlight.set(key, Math.max(0, (this.inFlight.get(key) ?? 1) - 1));
|
|
49
|
+
}
|
|
50
|
+
/** Acquire a slot for `key`, run `fn`, and always release — even on throw. */
|
|
51
|
+
async run(key, fn) {
|
|
52
|
+
await this.acquire(key);
|
|
53
|
+
try {
|
|
54
|
+
return await fn();
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
this.release(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-runtime-engine",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "AI Runtime
|
|
3
|
+
"version": "2.7.0",
|
|
4
|
+
"description": "AI Runtime \u2014 a provider-agnostic AI runtime and orchestration platform. Point it at whatever AI providers you have; it routes each task to the best available model. Ships the `ai-runtime` CLI and the `Runtime`/`AI` library API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"author": "pavan",
|
|
8
8
|
"keywords": [
|
|
9
|
-
"ai",
|
|
10
|
-
"
|
|
9
|
+
"ai",
|
|
10
|
+
"llm",
|
|
11
|
+
"ai-runtime",
|
|
12
|
+
"runtime",
|
|
13
|
+
"orchestration",
|
|
14
|
+
"router",
|
|
15
|
+
"provider-agnostic",
|
|
16
|
+
"agent",
|
|
17
|
+
"openai",
|
|
18
|
+
"gemini",
|
|
19
|
+
"groq",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"ollama",
|
|
22
|
+
"openrouter",
|
|
23
|
+
"mcp",
|
|
24
|
+
"cli",
|
|
25
|
+
"typescript"
|
|
11
26
|
],
|
|
12
27
|
"engines": {
|
|
13
28
|
"node": ">=22"
|
|
@@ -43,11 +58,11 @@
|
|
|
43
58
|
"scripts": {
|
|
44
59
|
"build": "tsc -p tsconfig.build.json",
|
|
45
60
|
"typecheck": "tsc --noEmit",
|
|
46
|
-
"test": "node --import tsx --test \"tests/**/*.test.ts\"",
|
|
47
|
-
"test:unit": "node --import tsx --test \"tests/unit/**/*.test.ts\"",
|
|
48
|
-
"test:integration": "node --import tsx --test \"tests/integration/**/*.test.ts\"",
|
|
49
|
-
"test:contract": "node --import tsx --test \"tests/contract/**/*.test.ts\"",
|
|
50
|
-
"test:chaos": "node --import tsx --test \"tests/chaos/**/*.test.ts\"",
|
|
61
|
+
"test": "node --import tsx --test --test-timeout=120000 \"tests/**/*.test.ts\"",
|
|
62
|
+
"test:unit": "node --import tsx --test --test-timeout=120000 \"tests/unit/**/*.test.ts\"",
|
|
63
|
+
"test:integration": "node --import tsx --test --test-timeout=120000 \"tests/integration/**/*.test.ts\"",
|
|
64
|
+
"test:contract": "node --import tsx --test --test-timeout=120000 \"tests/contract/**/*.test.ts\"",
|
|
65
|
+
"test:chaos": "node --import tsx --test --test-timeout=120000 \"tests/chaos/**/*.test.ts\"",
|
|
51
66
|
"cli": "node --import tsx src/cli/cli.ts",
|
|
52
67
|
"example:fallback": "node --import tsx examples/fallback.ts",
|
|
53
68
|
"example:minimal": "node --import tsx examples/minimal.ts",
|