mecord-connect 1.0.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/DISCLOSURE +47 -0
- package/LICENSE +64 -0
- package/README.md +44 -0
- package/bin/operator.mjs +10 -0
- package/package.json +58 -0
- package/runtime/app/apps/local-agent/src/approval-store.js +342 -0
- package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -0
- package/runtime/app/apps/local-agent/src/cli.js +237 -0
- package/runtime/app/apps/local-agent/src/device-reset.js +260 -0
- package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -0
- package/runtime/app/apps/local-agent/src/main.js +311 -0
- package/runtime/app/apps/local-agent/src/privacy-data.js +129 -0
- package/runtime/app/apps/local-agent/src/relay-agent.js +333 -0
- package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -0
- package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -0
- package/runtime/app/apps/local-agent/src/remote.js +130 -0
- package/runtime/app/apps/local-agent/src/runtime-factory.js +66 -0
- package/runtime/app/apps/local-agent/src/server.js +578 -0
- package/runtime/app/package.json +4 -0
- package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -0
- package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -0
- package/runtime/app/src/capabilities/browser-cdp-page.js +430 -0
- package/runtime/app/src/capabilities/browser-cdp.js +384 -0
- package/runtime/app/src/capabilities/browser-managed.js +345 -0
- package/runtime/app/src/capabilities/docker.js +448 -0
- package/runtime/app/src/capabilities/filesystem.js +264 -0
- package/runtime/app/src/capabilities/git-checkpoint.js +415 -0
- package/runtime/app/src/capabilities/git-write.js +321 -0
- package/runtime/app/src/capabilities/git.js +252 -0
- package/runtime/app/src/capabilities/index.js +16 -0
- package/runtime/app/src/capabilities/path-scope.js +86 -0
- package/runtime/app/src/capabilities/postgres.js +566 -0
- package/runtime/app/src/capabilities/process.js +258 -0
- package/runtime/app/src/capabilities/project-command.js +599 -0
- package/runtime/app/src/capabilities/project-transaction.js +215 -0
- package/runtime/app/src/capabilities/project.js +67 -0
- package/runtime/app/src/capabilities/system.js +40 -0
- package/runtime/app/src/capabilities/vscode.js +312 -0
- package/runtime/app/src/capabilities/windows-uia.js +331 -0
- package/runtime/app/src/core/account-device-registry.js +604 -0
- package/runtime/app/src/core/action-identity.js +44 -0
- package/runtime/app/src/core/audit.js +295 -0
- package/runtime/app/src/core/capability-policy.js +58 -0
- package/runtime/app/src/core/child-environment.js +29 -0
- package/runtime/app/src/core/device-enrollment.js +417 -0
- package/runtime/app/src/core/device-identity.js +436 -0
- package/runtime/app/src/core/device-registry.js +477 -0
- package/runtime/app/src/core/device-reset.js +178 -0
- package/runtime/app/src/core/device-routing.js +305 -0
- package/runtime/app/src/core/durable-state.js +306 -0
- package/runtime/app/src/core/errors.js +20 -0
- package/runtime/app/src/core/evidence.js +9 -0
- package/runtime/app/src/core/index.js +17 -0
- package/runtime/app/src/core/machine-state.js +160 -0
- package/runtime/app/src/core/network-authority.js +20 -0
- package/runtime/app/src/core/policy.js +67 -0
- package/runtime/app/src/core/product-identity.js +7 -0
- package/runtime/app/src/core/provenance.js +18 -0
- package/runtime/app/src/core/provider-learning.js +274 -0
- package/runtime/app/src/core/public-plugin-surface.js +25 -0
- package/runtime/app/src/core/public-restricted-data.js +105 -0
- package/runtime/app/src/core/rate-limit.js +118 -0
- package/runtime/app/src/core/relay-client.js +568 -0
- package/runtime/app/src/core/relay-delivery-store.js +512 -0
- package/runtime/app/src/core/relay-result-store.js +321 -0
- package/runtime/app/src/core/release-update.js +253 -0
- package/runtime/app/src/core/router.js +96 -0
- package/runtime/app/src/core/runtime.js +136 -0
- package/runtime/app/src/core/scoped-path-syntax.js +16 -0
- package/runtime/app/src/core/session-token.js +498 -0
- package/runtime/app/src/core/task-orchestrator.js +1148 -0
- package/runtime/app/src/core/task-store.js +517 -0
- package/runtime/app/src/core/task.js +142 -0
- package/runtime/app/src/core/trusted-executable.js +124 -0
- package/runtime/app/src/core/types.js +83 -0
- package/runtime/app/src/core/windows-path-lease.js +138 -0
- package/runtime/native/operator-windows-dpapi.exe +0 -0
- package/runtime/native/operator-windows-path-lease.exe +0 -0
- package/runtime/native/operator-windows-uia.exe +0 -0
- package/runtime/runtime-manifest.json +380 -0
- package/src/cli.mjs +345 -0
- package/src/publish-check.mjs +22 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const SAFE_STATE_KEYS = new Set([
|
|
6
|
+
'sha256', 'size', 'bytes', 'count', 'clean', 'operation', 'verified',
|
|
7
|
+
'exitCode', 'state', 'healthy', 'selected', 'expand_collapse_state', 'truncated',
|
|
8
|
+
'events_truncated', 'waited_ms', 'observed_ms', 'max_nodes', 'max_depth'
|
|
9
|
+
]);
|
|
10
|
+
const AMBIGUOUS_ERROR = /AMBIGUOUS|MULTIPLE_MATCH|TARGET_NOT_UNIQUE/i;
|
|
11
|
+
|
|
12
|
+
export function normalizeMachineObservation(
|
|
13
|
+
action ,
|
|
14
|
+
result ,
|
|
15
|
+
channel = 'semantic'
|
|
16
|
+
) {
|
|
17
|
+
const domain = observationDomain(result.capability, result.provider);
|
|
18
|
+
const importantState = importantStateFromResult(result);
|
|
19
|
+
const entityId = observationEntityId(action, result, domain);
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: 2,
|
|
22
|
+
channel,
|
|
23
|
+
domain,
|
|
24
|
+
provider: bounded(result.provider, 256),
|
|
25
|
+
capability: bounded(result.capability, 256),
|
|
26
|
+
entityId,
|
|
27
|
+
observedAt: new Date().toISOString(),
|
|
28
|
+
stateVersion: sha256(canonicalJson({ domain, entityId, importantState })),
|
|
29
|
+
importantState,
|
|
30
|
+
ambiguous: AMBIGUOUS_ERROR.test(result.error?.code ?? ''),
|
|
31
|
+
confidence: observationConfidence(result),
|
|
32
|
+
evidenceRefs: result.evidence.slice(0, 100).map(evidenceRef)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function observationDomain(capability , provider ) {
|
|
37
|
+
const lowerProvider = provider.toLowerCase();
|
|
38
|
+
if (lowerProvider.includes('uia')) return 'uia';
|
|
39
|
+
if (capability.startsWith('project.')) return 'project';
|
|
40
|
+
if (capability.startsWith('file.')) return 'filesystem';
|
|
41
|
+
if (capability.startsWith('git.')) return 'git';
|
|
42
|
+
if (capability.startsWith('docker.')) return 'docker';
|
|
43
|
+
if (capability.startsWith('postgres.')) return 'database';
|
|
44
|
+
if (capability.startsWith('vscode.')) return 'ide';
|
|
45
|
+
if (capability.startsWith('browser.')) return 'browser';
|
|
46
|
+
if (capability.startsWith('terminal.') || capability.startsWith('process.')) return 'process';
|
|
47
|
+
if (capability.startsWith('computer.')) return 'system';
|
|
48
|
+
if (capability.startsWith('app.')) return 'application';
|
|
49
|
+
return channelFromProvider(lowerProvider);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function channelFromProvider(provider ) {
|
|
53
|
+
if (provider.includes('docker')) return 'docker';
|
|
54
|
+
if (provider.includes('postgres')) return 'database';
|
|
55
|
+
if (provider.includes('vscode')) return 'ide';
|
|
56
|
+
return 'unknown';
|
|
57
|
+
}
|
|
58
|
+
function observationEntityId(action , result , domain ) {
|
|
59
|
+
const output = record(result.output);
|
|
60
|
+
const target = action.target
|
|
61
|
+
?? text(output.targetId)
|
|
62
|
+
?? text(record(output.target).id)
|
|
63
|
+
?? text(action.input.path)
|
|
64
|
+
?? text(action.input.cwd)
|
|
65
|
+
?? text(action.input.targetId)
|
|
66
|
+
?? canonicalJson(action.input.selector ?? {});
|
|
67
|
+
return `${domain}:${sha256(`${result.capability}\0${target || result.provider}`).slice(0, 32)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function importantStateFromResult(result ) {
|
|
71
|
+
const state = { ok: result.ok };
|
|
72
|
+
if (result.error?.code) state.errorCode = bounded(result.error.code, 128);
|
|
73
|
+
const output = record(result.output);
|
|
74
|
+
for (const [key, value] of Object.entries(output)) {
|
|
75
|
+
if (SAFE_STATE_KEYS.has(key) && isSafeScalar(value)) state[key] = value;
|
|
76
|
+
}
|
|
77
|
+
if (result.capability.startsWith('docker.')) {
|
|
78
|
+
const scope = text(output.scope);
|
|
79
|
+
if (scope === 'project' || scope === 'daemon') state.scope = scope;
|
|
80
|
+
for (const key of ['fingerprint', 'beforeFingerprint', 'afterFingerprint']) {
|
|
81
|
+
const value = text(output[key]);
|
|
82
|
+
if (value && /^[0-9a-f]{64}$/i.test(value)) state[key] = value.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
const services = normalizeDockerServices(output.services ?? output.states);
|
|
85
|
+
if (services.length) state.services = services;
|
|
86
|
+
}
|
|
87
|
+
if (result.capability.startsWith('postgres.')) {
|
|
88
|
+
const profileId = text(output.profileId);
|
|
89
|
+
const schema = text(output.schema);
|
|
90
|
+
const table = text(output.table);
|
|
91
|
+
if (profileId && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(profileId)) state.profileId = profileId;
|
|
92
|
+
if (schema && /^[A-Za-z_][A-Za-z0-9_$]{0,62}$/.test(schema)) state.schema = schema;
|
|
93
|
+
if (table && /^[A-Za-z_][A-Za-z0-9_$]{0,62}$/.test(table)) state.table = table;
|
|
94
|
+
for (const key of ['rowCount', 'limit', 'offset']) {
|
|
95
|
+
const value = Number(output[key]);
|
|
96
|
+
if (Number.isSafeInteger(value) && value >= 0 && value <= 10_000) state[key] = value;
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(output.columns)) {
|
|
99
|
+
const columns = output.columns.map(String).filter((column) => column === '*' || /^[A-Za-z_][A-Za-z0-9_$]{0,62}$/.test(column)).slice(0, 50);
|
|
100
|
+
if (columns.length) state.columns = columns;
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(output.profiles)) state.profileCount = Math.min(output.profiles.length, 1000);
|
|
103
|
+
if (Array.isArray(output.rows) && output.operation === 'columns') state.columnCount = Math.min(output.rows.length, 500);
|
|
104
|
+
}
|
|
105
|
+
const postcondition = record(output.postcondition);
|
|
106
|
+
const safePostcondition = {};
|
|
107
|
+
for (const [key, value] of Object.entries(postcondition)) {
|
|
108
|
+
if ((SAFE_STATE_KEYS.has(key) || key === 'focused' || key === 'foreground') && isSafeScalar(value)) safePostcondition[key] = value;
|
|
109
|
+
}
|
|
110
|
+
if (Object.keys(safePostcondition).length) state.postcondition = safePostcondition;
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeDockerServices(input ) {
|
|
115
|
+
if (!Array.isArray(input)) return [];
|
|
116
|
+
const output = [];
|
|
117
|
+
for (const item of input.slice(0, 50)) {
|
|
118
|
+
const raw = record(item);
|
|
119
|
+
const service = text(raw.service);
|
|
120
|
+
const containers = Number(raw.containers ?? 0);
|
|
121
|
+
const states = Array.isArray(raw.states)
|
|
122
|
+
? [...new Set(raw.states.map(String).filter((state) => /^[a-z][a-z0-9_-]{0,63}$/i.test(state)))].sort().slice(0, 20)
|
|
123
|
+
: [];
|
|
124
|
+
if (!service || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(service)) continue;
|
|
125
|
+
if (!Number.isSafeInteger(containers) || containers < 0 || containers > 1000) continue;
|
|
126
|
+
output.push({ service, containers, states });
|
|
127
|
+
}
|
|
128
|
+
return output.sort((a, b) => a.service.localeCompare(b.service));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function observationConfidence(result ) {
|
|
132
|
+
if (AMBIGUOUS_ERROR.test(result.error?.code ?? '')) return 0;
|
|
133
|
+
return result.ok ? 1 : 0.5;
|
|
134
|
+
}
|
|
135
|
+
function evidenceRef(item ) {
|
|
136
|
+
return sha256(canonicalJson({ kind: item.kind, status: item.status, timestamp: item.timestamp }));
|
|
137
|
+
}
|
|
138
|
+
function isSafeScalar(value ) {
|
|
139
|
+
return value === null || typeof value === 'boolean' || typeof value === 'number'
|
|
140
|
+
|| (typeof value === 'string' && value.length <= 256 && !/[\r\n\0]/.test(value));
|
|
141
|
+
}
|
|
142
|
+
function record(value ) {
|
|
143
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
144
|
+
}
|
|
145
|
+
function text(value ) {
|
|
146
|
+
return typeof value === 'string' && value.length <= 4096 ? value : undefined;
|
|
147
|
+
}
|
|
148
|
+
function bounded(value , max ) {
|
|
149
|
+
return value.slice(0, max).replace(/[\r\n\0]/g, ' ');
|
|
150
|
+
}
|
|
151
|
+
function sha256(value ) {
|
|
152
|
+
return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
153
|
+
}
|
|
154
|
+
function canonicalJson(value ) {
|
|
155
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
156
|
+
if (!value || typeof value !== 'object') return JSON.stringify(value);
|
|
157
|
+
const object = value ;
|
|
158
|
+
return `{${Object.keys(object).sort().filter((key) => object[key] !== undefined)
|
|
159
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(',')}}`;
|
|
160
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
import { OperatorError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
export function requireLiteralLoopbackBindHost(input , label = 'Local service') {
|
|
5
|
+
const host = String(input ?? '').trim().toLowerCase();
|
|
6
|
+
if (host === '127.0.0.1') return host;
|
|
7
|
+
if (host === '::1' || host === '[::1]') return '::1';
|
|
8
|
+
throw new OperatorError(
|
|
9
|
+
'UNSAFE_LOCAL_BIND_HOST',
|
|
10
|
+
`${label} must bind to a literal loopback address (127.0.0.1 or ::1). Remote ingress must use the approved relay/tunnel boundary.`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function applyBoundedHttpServerPolicy(server ) {
|
|
15
|
+
server.headersTimeout = 10_000;
|
|
16
|
+
server.requestTimeout = 30_000;
|
|
17
|
+
server.keepAliveTimeout = 5_000;
|
|
18
|
+
server.maxRequestsPerSocket = 100;
|
|
19
|
+
server.maxHeadersCount = 64;
|
|
20
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { PolicyError } from './errors.js';
|
|
4
|
+
import { assertInstructionAuthority } from './provenance.js';
|
|
5
|
+
import { assertCanonicalRisk, capabilityRiskRule } from './capability-policy.js';
|
|
6
|
+
import { normalizeScopedPathSyntax } from './scoped-path-syntax.js';
|
|
7
|
+
|
|
8
|
+
function capabilityAllowed(capability , allowed ) {
|
|
9
|
+
return allowed.some((rule) => rule === capability || (rule.endsWith('.*') && capability.startsWith(rule.slice(0, -1))));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pathWithin(child , root ) {
|
|
13
|
+
const rel = path.relative(path.resolve(root), child);
|
|
14
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function policyPath(inputPath , roots ) {
|
|
18
|
+
const syntax = normalizeScopedPathSyntax(inputPath);
|
|
19
|
+
if (syntax.kind === 'native-absolute') return syntax.value;
|
|
20
|
+
if (syntax.kind === 'foreign-windows-absolute' || roots.length !== 1) return undefined;
|
|
21
|
+
return path.resolve(roots[0] , syntax.value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class PolicyEngine {
|
|
25
|
+
authorizeBase(action , permissions ) {
|
|
26
|
+
assertInstructionAuthority(action.provenance);
|
|
27
|
+
|
|
28
|
+
if (!capabilityAllowed(action.capability, permissions.allowedCapabilities)) {
|
|
29
|
+
throw new PolicyError('CAPABILITY_DENIED', `Capability ${action.capability} is not permitted.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const targetPath = typeof action.input.path === 'string'
|
|
33
|
+
? action.input.path
|
|
34
|
+
: typeof action.input.cwd === 'string'
|
|
35
|
+
? action.input.cwd
|
|
36
|
+
: undefined;
|
|
37
|
+
|
|
38
|
+
if (targetPath && permissions.allowedRoots.length > 0) {
|
|
39
|
+
const candidate = policyPath(targetPath, permissions.allowedRoots);
|
|
40
|
+
if (!candidate || !permissions.allowedRoots.some((root) => pathWithin(candidate, root))) {
|
|
41
|
+
throw new PolicyError('PATH_OUTSIDE_SCOPE', 'Requested path is outside the authorized roots.', { targetPath });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
authorizeRisk(action , permissions ) {
|
|
48
|
+
const approved = new Set(permissions.approvedActionIds ?? []);
|
|
49
|
+
if (action.risk === 'external' && !permissions.allowExternalWrites && !approved.has(action.id)) {
|
|
50
|
+
throw new PolicyError('APPROVAL_REQUIRED', 'External write requires explicit approval.', { actionId: action.id });
|
|
51
|
+
}
|
|
52
|
+
if (action.risk === 'system' && !permissions.allowSystemChanges && !approved.has(action.id)) {
|
|
53
|
+
throw new PolicyError('APPROVAL_REQUIRED', 'System change requires explicit approval.', { actionId: action.id });
|
|
54
|
+
}
|
|
55
|
+
if (action.risk === 'destructive' && !permissions.allowDestructive && !approved.has(action.id)) {
|
|
56
|
+
throw new PolicyError('APPROVAL_REQUIRED', 'Destructive action requires explicit approval.', { actionId: action.id });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
authorize(action , permissions ) {
|
|
61
|
+
this.authorizeBase(action, permissions);
|
|
62
|
+
const rule = capabilityRiskRule(action.capability);
|
|
63
|
+
if (rule === 'dynamic') throw new PolicyError('CAPABILITY_RISK_UNRESOLVED', `Capability ${action.capability} requires trusted dynamic risk resolution.`);
|
|
64
|
+
assertCanonicalRisk(action, rule);
|
|
65
|
+
this.authorizeRisk(action, permissions);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const PRODUCT_NAME = 'mecord-connect';
|
|
2
|
+
export const PRODUCT_TITLE = 'Mecord Connect';
|
|
3
|
+
export const PRODUCT_VERSION = '1.0.0';
|
|
4
|
+
|
|
5
|
+
export function runtimeProductIdentity() {
|
|
6
|
+
return { name: PRODUCT_NAME, title: PRODUCT_TITLE, version: PRODUCT_VERSION };
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import { PolicyError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
const INSTRUCTION_AUTHORITIES = new Set(['user', 'chatgpt', 'trusted_policy', 'runtime']);
|
|
5
|
+
|
|
6
|
+
export function assertInstructionAuthority(provenance ) {
|
|
7
|
+
if (!INSTRUCTION_AUTHORITIES.has(provenance.kind)) {
|
|
8
|
+
throw new PolicyError(
|
|
9
|
+
'UNTRUSTED_INSTRUCTION_SOURCE',
|
|
10
|
+
`Content from ${provenance.kind} is observation data and cannot redefine task goals or permissions.`,
|
|
11
|
+
{ provenance }
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isObservedContent(provenance ) {
|
|
17
|
+
return !INSTRUCTION_AUTHORITIES.has(provenance.kind);
|
|
18
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { OperatorError } from './errors.js';
|
|
3
|
+
import { readDurableStateText, writeDurableStateText } from './durable-state.js';
|
|
4
|
+
|
|
5
|
+
const MAX_ENTRIES = 512;
|
|
6
|
+
const MAX_COUNTER_TOTAL = 1000;
|
|
7
|
+
const MAX_ADJUSTMENT = 0.06;
|
|
8
|
+
const MAX_RELIABILITY_ADJUSTMENT = 0.05;
|
|
9
|
+
const MAX_LATENCY_PENALTY = 0.01;
|
|
10
|
+
const MAX_LATENCY_MS = 60 * 60_000;
|
|
11
|
+
const STATE_OPTIONS = {
|
|
12
|
+
maxBytes: 512 * 1024,
|
|
13
|
+
errorCode: 'PROVIDER_LEARNING_STATE_CORRUPT',
|
|
14
|
+
invalidMessage: 'Provider learning state is invalid.'
|
|
15
|
+
} ;
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export class ProviderLearningStore {
|
|
44
|
+
#file ;
|
|
45
|
+
#clock ;
|
|
46
|
+
#serial = Promise.resolve();
|
|
47
|
+
|
|
48
|
+
constructor(stateDir , options = {}) {
|
|
49
|
+
this.#file = path.join(path.resolve(stateDir), 'provider-learning.json');
|
|
50
|
+
this.#clock = options.clock ?? (() => new Date());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async adjustment(capabilityInput , providerInput , contextInput = 'global') {
|
|
54
|
+
const capability = boundedKey(capabilityInput, 'capability');
|
|
55
|
+
const provider = boundedKey(providerInput, 'provider');
|
|
56
|
+
const context = boundedContext(contextInput);
|
|
57
|
+
await this.#serial;
|
|
58
|
+
const state = await this.#read();
|
|
59
|
+
const exact = state.entries.find((item) => item.capability === capability && item.provider === provider && item.context === context);
|
|
60
|
+
const fallback = context === 'global'
|
|
61
|
+
? undefined
|
|
62
|
+
: state.entries.find((item) => item.capability === capability && item.provider === provider && item.context === 'global');
|
|
63
|
+
const entry = exact ?? fallback;
|
|
64
|
+
return entry ? learnedAdjustment(entry.verified, entry.failed, entry.latencyEwmaMs, entry.latencySamples) : 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async record(
|
|
68
|
+
capabilityInput ,
|
|
69
|
+
providerInput ,
|
|
70
|
+
outcome ,
|
|
71
|
+
metadata = {}
|
|
72
|
+
) {
|
|
73
|
+
const capability = boundedKey(capabilityInput, 'capability');
|
|
74
|
+
const provider = boundedKey(providerInput, 'provider');
|
|
75
|
+
const context = boundedContext(metadata.context ?? 'global');
|
|
76
|
+
const durationMs = boundedDuration(metadata.durationMs);
|
|
77
|
+
if (outcome !== 'verified' && outcome !== 'failed') throw new OperatorError('PROVIDER_LEARNING_INPUT_INVALID', 'Provider learning outcome is invalid.');
|
|
78
|
+
|
|
79
|
+
const run = this.#serial.then(async () => {
|
|
80
|
+
const state = await this.#read();
|
|
81
|
+
let entry = state.entries.find((item) => item.capability === capability && item.provider === provider && item.context === context);
|
|
82
|
+
if (!entry) {
|
|
83
|
+
if (state.entries.length >= MAX_ENTRIES) {
|
|
84
|
+
state.entries.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt) || entryKey(a).localeCompare(entryKey(b)));
|
|
85
|
+
state.entries.shift();
|
|
86
|
+
}
|
|
87
|
+
entry = {
|
|
88
|
+
capability, provider, context,
|
|
89
|
+
verified: 0, failed: 0,
|
|
90
|
+
latencyEwmaMs: 0, latencySamples: 0,
|
|
91
|
+
updatedAt: this.#clock().toISOString()
|
|
92
|
+
};
|
|
93
|
+
state.entries.push(entry);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (entry.verified + entry.failed >= MAX_COUNTER_TOTAL) {
|
|
97
|
+
entry.verified = Math.floor(entry.verified / 2);
|
|
98
|
+
entry.failed = Math.floor(entry.failed / 2);
|
|
99
|
+
}
|
|
100
|
+
if (outcome === 'verified') entry.verified += 1;
|
|
101
|
+
else entry.failed += 1;
|
|
102
|
+
|
|
103
|
+
if (durationMs !== undefined) {
|
|
104
|
+
entry.latencyEwmaMs = entry.latencySamples === 0
|
|
105
|
+
? durationMs
|
|
106
|
+
: Math.round((entry.latencyEwmaMs * 0.8 + durationMs * 0.2) * 1000) / 1000;
|
|
107
|
+
entry.latencySamples = Math.min(MAX_COUNTER_TOTAL, entry.latencySamples + 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
entry.updatedAt = this.#clock().toISOString();
|
|
111
|
+
state.entries.sort((a, b) => entryKey(a).localeCompare(entryKey(b)));
|
|
112
|
+
await writeDurableStateText(this.#file, JSON.stringify(state, null, 2), STATE_OPTIONS);
|
|
113
|
+
});
|
|
114
|
+
this.#serial = run.catch(() => undefined);
|
|
115
|
+
return await run;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async #read() {
|
|
119
|
+
let text ;
|
|
120
|
+
try {
|
|
121
|
+
text = await readDurableStateText(this.#file, STATE_OPTIONS);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if ((error ).code === 'ENOENT') return { version: 2, entries: [] };
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let decoded ;
|
|
128
|
+
try { decoded = JSON.parse(text); }
|
|
129
|
+
catch { throw corrupt('State is not valid JSON.'); }
|
|
130
|
+
return validateState(decoded);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function learnedAdjustment(verified , failed , latencyEwmaMs = 0, latencySamples = 0) {
|
|
135
|
+
const successes = boundedCount(verified);
|
|
136
|
+
const failures = boundedCount(failed);
|
|
137
|
+
const total = successes + failures;
|
|
138
|
+
|
|
139
|
+
let reliabilityAdjustment = 0;
|
|
140
|
+
if (total >= 2) {
|
|
141
|
+
const posteriorSuccess = (successes + 2) / (total + 4);
|
|
142
|
+
const centered = (posteriorSuccess - 0.5) * 2;
|
|
143
|
+
const confidence = Math.min(1, total / 20);
|
|
144
|
+
reliabilityAdjustment = centered * confidence * MAX_RELIABILITY_ADJUSTMENT;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const samples = boundedCount(latencySamples);
|
|
148
|
+
const latency = boundedLatencyState(latencyEwmaMs);
|
|
149
|
+
let latencyPenalty = 0;
|
|
150
|
+
if (samples >= 3 && latency > 100) {
|
|
151
|
+
const normalized = clamp((Math.log10(latency) - 2) / 3, 0, 1);
|
|
152
|
+
latencyPenalty = normalized * MAX_LATENCY_PENALTY;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return clamp(reliabilityAdjustment - latencyPenalty, -MAX_ADJUSTMENT, MAX_ADJUSTMENT);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validateState(input ) {
|
|
159
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw corrupt('State must be an object.');
|
|
160
|
+
const raw = input ;
|
|
161
|
+
if (!Array.isArray(raw.entries) || raw.entries.length > MAX_ENTRIES) {
|
|
162
|
+
throw corrupt(`State must contain at most ${MAX_ENTRIES} entries.`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (raw.version === 1) {
|
|
166
|
+
const seen = new Set ();
|
|
167
|
+
const entries = raw.entries.map((value, index) => {
|
|
168
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw corrupt(`Entry ${index} must be an object.`);
|
|
169
|
+
const item = value ;
|
|
170
|
+
const entry = {
|
|
171
|
+
capability: storedKey(item.capability, `entry ${index} capability`),
|
|
172
|
+
provider: storedKey(item.provider, `entry ${index} provider`),
|
|
173
|
+
context: 'global',
|
|
174
|
+
verified: boundedCount(item.verified),
|
|
175
|
+
failed: boundedCount(item.failed),
|
|
176
|
+
latencyEwmaMs: 0,
|
|
177
|
+
latencySamples: 0,
|
|
178
|
+
updatedAt: validIso(item.updatedAt, `entry ${index} updatedAt`)
|
|
179
|
+
};
|
|
180
|
+
const key = entryKey(entry);
|
|
181
|
+
if (seen.has(key)) throw corrupt(`Entry ${index} duplicates a capability/provider/context tuple.`);
|
|
182
|
+
seen.add(key);
|
|
183
|
+
return entry;
|
|
184
|
+
});
|
|
185
|
+
return { version: 2, entries };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (raw.version !== 2) throw corrupt('State version must be 1 or 2.');
|
|
189
|
+
const seen = new Set ();
|
|
190
|
+
const entries = raw.entries.map((value, index) => {
|
|
191
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw corrupt(`Entry ${index} must be an object.`);
|
|
192
|
+
const item = value ;
|
|
193
|
+
const entry = {
|
|
194
|
+
capability: storedKey(item.capability, `entry ${index} capability`),
|
|
195
|
+
provider: storedKey(item.provider, `entry ${index} provider`),
|
|
196
|
+
context: storedContext(item.context, `entry ${index} context`),
|
|
197
|
+
verified: boundedCount(item.verified),
|
|
198
|
+
failed: boundedCount(item.failed),
|
|
199
|
+
latencyEwmaMs: boundedLatencyState(item.latencyEwmaMs),
|
|
200
|
+
latencySamples: boundedCount(item.latencySamples),
|
|
201
|
+
updatedAt: validIso(item.updatedAt, `entry ${index} updatedAt`)
|
|
202
|
+
};
|
|
203
|
+
const key = entryKey(entry);
|
|
204
|
+
if (seen.has(key)) throw corrupt(`Entry ${index} duplicates a capability/provider/context tuple.`);
|
|
205
|
+
seen.add(key);
|
|
206
|
+
return entry;
|
|
207
|
+
});
|
|
208
|
+
return { version: 2, entries };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function entryKey(entry ) {
|
|
212
|
+
return `${entry.capability}\0${entry.provider}\0${entry.context}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function storedKey(input , label ) {
|
|
216
|
+
try { return boundedKey(input, label); }
|
|
217
|
+
catch { throw corrupt(`${label} is invalid.`); }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function storedContext(input , label ) {
|
|
221
|
+
try { return boundedContext(input); }
|
|
222
|
+
catch { throw corrupt(`${label} is invalid.`); }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function boundedKey(input , label ) {
|
|
226
|
+
if (typeof input !== 'string' || input.length < 1 || input.length > 256 || input.includes('\0') || /[\r\n]/.test(input)) {
|
|
227
|
+
throw new OperatorError('PROVIDER_LEARNING_INPUT_INVALID', `${label} is invalid.`);
|
|
228
|
+
}
|
|
229
|
+
return input;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function boundedContext(input ) {
|
|
233
|
+
const value = String(input ?? 'global');
|
|
234
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
|
|
235
|
+
throw new OperatorError('PROVIDER_LEARNING_INPUT_INVALID', 'learning context is invalid.');
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function boundedDuration(input ) {
|
|
241
|
+
if (input === undefined) return undefined;
|
|
242
|
+
const value = Number(input);
|
|
243
|
+
if (!Number.isFinite(value) || value < 0 || value > MAX_LATENCY_MS) {
|
|
244
|
+
throw new OperatorError('PROVIDER_LEARNING_INPUT_INVALID', 'learning duration is invalid.');
|
|
245
|
+
}
|
|
246
|
+
return Math.round(value * 1000) / 1000;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function boundedLatencyState(input ) {
|
|
250
|
+
const value = Number(input ?? 0);
|
|
251
|
+
if (!Number.isFinite(value) || value < 0 || value > MAX_LATENCY_MS) throw corrupt('Learning latency is invalid.');
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function boundedCount(input ) {
|
|
256
|
+
const value = Number(input);
|
|
257
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_COUNTER_TOTAL) throw corrupt('Learning counters are invalid.');
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function validIso(input , label ) {
|
|
262
|
+
const value = String(input ?? '');
|
|
263
|
+
const parsed = Date.parse(value);
|
|
264
|
+
if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) throw corrupt(`${label} must be an ISO timestamp.`);
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function clamp(value , min , max ) {
|
|
269
|
+
return Math.max(min, Math.min(max, value));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function corrupt(message ) {
|
|
273
|
+
return new OperatorError('PROVIDER_LEARNING_STATE_CORRUPT', `Provider learning state is invalid. ${message}`);
|
|
274
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Dependency-free contract shared by root publication tests and the MCP adapter.
|
|
2
|
+
// Keep this list limited to tools that are intentionally exposed by the public plugin.
|
|
3
|
+
export const PUBLIC_PLUGIN_SURFACE_VERSION = 1;
|
|
4
|
+
export const PUBLIC_PLUGIN_TOOL_NAMES = Object.freeze([
|
|
5
|
+
'computer.inspect',
|
|
6
|
+
'project.inspect',
|
|
7
|
+
'project.commands',
|
|
8
|
+
'file.list',
|
|
9
|
+
'file.read',
|
|
10
|
+
'file.create',
|
|
11
|
+
'file.replace',
|
|
12
|
+
'git.status',
|
|
13
|
+
'git.diff'
|
|
14
|
+
].sort());
|
|
15
|
+
export const PUBLIC_PLUGIN_CAPABILITIES = Object.freeze([
|
|
16
|
+
'computer.inspect',
|
|
17
|
+
'project.inspect',
|
|
18
|
+
'project.command.inspect',
|
|
19
|
+
'file.list',
|
|
20
|
+
'file.read',
|
|
21
|
+
'file.create',
|
|
22
|
+
'file.replace',
|
|
23
|
+
'git.status',
|
|
24
|
+
'git.diff'
|
|
25
|
+
].sort());
|