Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
402 lines
13 KiB
JavaScript
402 lines
13 KiB
JavaScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
const SAFE_TRACE_KEYS = new Set([
|
|
'type',
|
|
'actionType',
|
|
'path',
|
|
'effect',
|
|
'verified',
|
|
'supported',
|
|
'ok',
|
|
'durationMs',
|
|
'at',
|
|
'toolCallId',
|
|
'pid',
|
|
'windowId',
|
|
'expectedPid',
|
|
'expectedWindowId',
|
|
'winnerPid',
|
|
'winnerWindowId',
|
|
'winnerZIndex',
|
|
'address',
|
|
'tool',
|
|
]);
|
|
const SAFE_TRACE_TYPES = new Set([
|
|
'dispatch',
|
|
'fallback',
|
|
'occlusion',
|
|
'outcome',
|
|
'semantic_result',
|
|
'snapshot',
|
|
'target',
|
|
'tool_start',
|
|
'tool_result',
|
|
'complete',
|
|
'abort',
|
|
'error',
|
|
]);
|
|
const SAFE_ACTION_TYPES = new Set([
|
|
'list_apps',
|
|
'observe',
|
|
'click_element',
|
|
'set_value',
|
|
'select_text',
|
|
'secondary_action',
|
|
'press_key',
|
|
'screenshot',
|
|
'cursor_position',
|
|
'mouse_move',
|
|
'left_click',
|
|
'right_click',
|
|
'middle_click',
|
|
'double_click',
|
|
'triple_click',
|
|
'left_mouse_down',
|
|
'left_mouse_up',
|
|
'left_click_drag',
|
|
'type',
|
|
'key',
|
|
'hold_key',
|
|
'scroll',
|
|
'wait',
|
|
'zoom',
|
|
'unknown',
|
|
]);
|
|
const SAFE_TRACE_PATHS = new Set(['ax', 'cdp', 'cgevent', 'screenshot-detail']);
|
|
const SAFE_TRACE_EFFECTS = new Set(['confirmed', 'unverifiable']);
|
|
const SAFE_DISPATCH_ADDRESSES = new Set(['ax', 'px', 'semantic', 'none']);
|
|
const SAFE_DISPATCH_TOOLS = new Set([
|
|
'click',
|
|
'set_value',
|
|
'page',
|
|
'press_key',
|
|
'scroll',
|
|
'drag',
|
|
'zoom',
|
|
]);
|
|
|
|
function safeUrlOrigin(value) {
|
|
try {
|
|
return new URL(value).origin;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function actionType(action) {
|
|
return typeof action?.type === 'string'
|
|
? action.type
|
|
: typeof action?.action === 'string'
|
|
? action.action
|
|
: 'unknown';
|
|
}
|
|
|
|
function safeToken(value, allowed) {
|
|
return typeof value === 'string' && allowed.has(value) ? value : undefined;
|
|
}
|
|
|
|
function resultCode(text) {
|
|
if (typeof text !== 'string') return undefined;
|
|
const failed = text.match(/\bfailed:\s*([a-z][a-z0-9_]{1,63})\b/i);
|
|
if (failed) return failed[1];
|
|
const status = text.match(/\b(?:ok|error)=([a-z][a-z0-9_]{1,63})\b/i);
|
|
return status?.[1];
|
|
}
|
|
|
|
export function sanitizeCuActionRecord(record) {
|
|
const type = actionType(record?.action ?? record);
|
|
return {
|
|
type: SAFE_ACTION_TYPES.has(type) ? type : 'unknown',
|
|
...(safeId(record?.toolCallId) ? { toolCallId: safeId(record.toolCallId) } : {}),
|
|
...(safeId(record?.sourceObservationId)
|
|
? { sourceObservationId: safeId(record.sourceObservationId) }
|
|
: {}),
|
|
...(type !== 'screenshot' && safeId(record?.resultObservationId)
|
|
? { resultObservationId: safeId(record.resultObservationId) }
|
|
: {}),
|
|
...(Number.isInteger(record?.targetPid) && record.targetPid > 0
|
|
? { targetPid: record.targetPid }
|
|
: {}),
|
|
...(Number.isInteger(record?.targetWindowId) && record.targetWindowId > 0
|
|
? { targetWindowId: record.targetWindowId }
|
|
: {}),
|
|
...(typeof record?.success === 'boolean' ? { success: record.success } : {}),
|
|
...(typeof record?.targetOwned === 'boolean' ? { targetOwned: record.targetOwned } : {}),
|
|
...(record?.expectedFailure === true ? { expectedFailure: true } : {}),
|
|
...(Number.isFinite(record?.durationMs) ? { durationMs: record.durationMs } : {}),
|
|
...(Number.isFinite(record?.modelLatencyMs) ? { modelLatencyMs: record.modelLatencyMs } : {}),
|
|
...(Number.isFinite(record?.toolLatencyMs) ? { toolLatencyMs: record.toolLatencyMs } : {}),
|
|
...(Number.isFinite(record?.displayLagMs) ? { displayLagMs: record.displayLagMs } : {}),
|
|
...(resultCode(record?.text) ? { resultCode: resultCode(record.text) } : {}),
|
|
};
|
|
}
|
|
|
|
export function sanitizeCuTrace(trace) {
|
|
if (!trace || typeof trace !== 'object') return null;
|
|
if (!SAFE_TRACE_TYPES.has(trace.type)) return null;
|
|
const sanitized = {};
|
|
for (const [key, value] of Object.entries(trace)) {
|
|
if (!SAFE_TRACE_KEYS.has(key)) continue;
|
|
if (key === 'type') {
|
|
sanitized.type = trace.type;
|
|
continue;
|
|
}
|
|
if (key === 'actionType') {
|
|
const action = safeToken(value, SAFE_ACTION_TYPES);
|
|
if (action) sanitized.actionType = action;
|
|
continue;
|
|
}
|
|
if (key === 'path') {
|
|
const path = safeToken(value, SAFE_TRACE_PATHS);
|
|
if (path) sanitized.path = path;
|
|
continue;
|
|
}
|
|
if (key === 'effect') {
|
|
const effect = safeToken(value, SAFE_TRACE_EFFECTS);
|
|
if (effect) sanitized.effect = effect;
|
|
continue;
|
|
}
|
|
if (key === 'address') {
|
|
const address = safeToken(value, SAFE_DISPATCH_ADDRESSES);
|
|
if (address) sanitized.address = address;
|
|
continue;
|
|
}
|
|
if (key === 'tool') {
|
|
const tool = safeToken(value, SAFE_DISPATCH_TOOLS);
|
|
if (tool) sanitized.tool = tool;
|
|
continue;
|
|
}
|
|
if (typeof value === 'string' || typeof value === 'boolean' || Number.isFinite(value)) {
|
|
sanitized[key] = value;
|
|
}
|
|
}
|
|
return Object.keys(sanitized).length > 0 ? sanitized : null;
|
|
}
|
|
|
|
export function sanitizeCuDirectReport(report) {
|
|
return sanitizeCuReport({
|
|
...report,
|
|
endpointOrigin: safeUrlOrigin(report?.endpointOrigin ?? report?.baseUrl),
|
|
});
|
|
}
|
|
|
|
export function sanitizeCuReport(report) {
|
|
const terminal = report?.terminal;
|
|
const fixtureState = report?.fixtureState;
|
|
const expectedState = Array.isArray(report?.expectedState)
|
|
? report.expectedState.map(sanitizeAssertionResult).filter(Boolean)
|
|
: [];
|
|
const violations = Array.isArray(report?.forbiddenEffects?.violations)
|
|
? report.forbiddenEffects.violations.map(sanitizeAssertionResult).filter(Boolean)
|
|
: [];
|
|
return {
|
|
schemaVersion: report?.schemaVersion === 1 ? 1 : undefined,
|
|
runId: safeId(report?.runId),
|
|
gitRevision: safeGitRevision(report?.gitRevision),
|
|
generatedAt: safeTimestamp(report?.generatedAt),
|
|
contentLineage: sanitizeContentLineage(report?.contentLineage),
|
|
evidenceClass: safeToken(
|
|
report?.evidenceClass,
|
|
new Set(['real-runtime', 'fault-injection', 'hermetic-protocol', 'static-contract']),
|
|
),
|
|
scenarioId: safeId(report?.scenarioId),
|
|
producer: safeId(report?.producer),
|
|
transportClass: safeToken(
|
|
report?.transportClass,
|
|
new Set(['live-network', 'hermetic', 'static']),
|
|
),
|
|
policyMode: safeToken(report?.policyMode, new Set(['enforced', 'bypassed'])),
|
|
qualificationEligible: report?.qualificationEligible === true,
|
|
deprecated: report?.deprecated === true,
|
|
toolExposure: safeToken(report?.toolExposure, new Set(['direct-e2e', 'deferred'])),
|
|
provider: safeId(report?.provider),
|
|
model: safeId(report?.model),
|
|
endpointOrigin: safeUrlOrigin(report.baseUrl),
|
|
status: safeToken(report?.status, new Set(['pass', 'fail', 'inconclusive'])),
|
|
terminal:
|
|
terminal && typeof terminal === 'object'
|
|
? {
|
|
type: safeToken(terminal.type, new Set(['complete', 'abort', 'error'])),
|
|
stopReason: safeToken(
|
|
terminal.stopReason,
|
|
new Set([
|
|
'end_turn',
|
|
'max_tokens',
|
|
'step_limit',
|
|
'error',
|
|
'user_stop',
|
|
'permission_handoff',
|
|
]),
|
|
),
|
|
}
|
|
: undefined,
|
|
fixtureIdentity: sanitizeFixtureIdentity(report?.fixtureIdentity),
|
|
faultInjection: sanitizeFaultInjection(report?.faultInjection),
|
|
run:
|
|
report?.run && typeof report.run === 'object'
|
|
? {
|
|
status: safeToken(
|
|
report.run.status,
|
|
new Set([
|
|
'created',
|
|
'running',
|
|
'waiting_for_user',
|
|
'completed',
|
|
'failed',
|
|
'cancelled',
|
|
]),
|
|
),
|
|
failureClass: safeId(report.run.failureClass),
|
|
...(Number.isFinite(report.run.durationMs)
|
|
? { durationMs: report.run.durationMs }
|
|
: {}),
|
|
}
|
|
: undefined,
|
|
...(Number.isFinite(report?.totalLatencyMs) ? { totalLatencyMs: report.totalLatencyMs } : {}),
|
|
actionAttempts: Number.isInteger(report?.actionAttempts) ? report.actionAttempts : undefined,
|
|
actionCount: Number.isInteger(report?.actionCount) ? report.actionCount : undefined,
|
|
actionCounts: sanitizeCountMap(report?.actionCounts),
|
|
minimumActionsPassed: report?.minimumActionsPassed === true,
|
|
actionsWithinBudget: report?.actionsWithinBudget === true,
|
|
dispatchPathPassed: report?.dispatchPathPassed === true,
|
|
actions: Array.isArray(report?.actions) ? report.actions.map(sanitizeCuActionRecord) : [],
|
|
fixtureState: sanitizeFixtureState(fixtureState),
|
|
expectedState,
|
|
forbiddenEffects: {
|
|
status: safeToken(report?.forbiddenEffects?.status, new Set(['pass', 'fail'])),
|
|
violations,
|
|
},
|
|
traces: Array.isArray(report?.traces) ? report.traces.map(sanitizeCuTrace).filter(Boolean) : [],
|
|
driverTraces: Array.isArray(report?.driverTraces)
|
|
? report.driverTraces.map(sanitizeCuTrace).filter(Boolean)
|
|
: [],
|
|
};
|
|
}
|
|
|
|
function sanitizeContentLineage(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
const generator = safeScriptPath(value.generator);
|
|
const gitRevision = safeGitRevision(value.gitRevision);
|
|
const generatedAt = safeTimestamp(value.generatedAt);
|
|
if (!generator || !gitRevision || !generatedAt) return undefined;
|
|
return { generator, gitRevision, generatedAt };
|
|
}
|
|
|
|
function safeScriptPath(value) {
|
|
return typeof value === 'string' && /^scripts\/[A-Za-z0-9._/-]{1,192}$/.test(value)
|
|
? value
|
|
: undefined;
|
|
}
|
|
|
|
function safeGitRevision(value) {
|
|
return typeof value === 'string' && /^[a-f0-9]{40}$/.test(value) ? value : undefined;
|
|
}
|
|
|
|
function safeTimestamp(value) {
|
|
if (typeof value !== 'string') return undefined;
|
|
const timestamp = Date.parse(value);
|
|
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value
|
|
? value
|
|
: undefined;
|
|
}
|
|
|
|
function sanitizeFixtureIdentity(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
const candidates = Array.isArray(value.instances) ? value.instances : [value];
|
|
const instances = candidates.flatMap((candidate) => {
|
|
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return [];
|
|
const pid = Number.isInteger(candidate.pid) && candidate.pid > 0 ? candidate.pid : undefined;
|
|
const windowIds = Array.isArray(candidate.windowIds)
|
|
? [...new Set(candidate.windowIds.filter((entry) => Number.isInteger(entry) && entry > 0))]
|
|
: [];
|
|
return pid && windowIds.length > 0 ? [{ pid, windowIds }] : [];
|
|
});
|
|
if (instances.length === 0) return undefined;
|
|
return {
|
|
instances: instances.filter(
|
|
(candidate, index) =>
|
|
instances.findIndex(
|
|
(entry) =>
|
|
entry.pid === candidate.pid &&
|
|
JSON.stringify(entry.windowIds) === JSON.stringify(candidate.windowIds),
|
|
) === index,
|
|
),
|
|
};
|
|
}
|
|
|
|
function sanitizeFaultInjection(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
const layer = safeToken(value.layer, new Set(['runtime']));
|
|
const kind = safeId(value.kind);
|
|
return layer && kind ? { layer, kind } : undefined;
|
|
}
|
|
|
|
function safeId(value) {
|
|
return typeof value === 'string' && /^[A-Za-z0-9._-]{1,128}$/.test(value) ? value : undefined;
|
|
}
|
|
|
|
function sanitizeCountMap(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
return Object.fromEntries(
|
|
Object.entries(value).flatMap(([key, count]) =>
|
|
SAFE_ACTION_TYPES.has(key) && Number.isInteger(count) && count >= 0 ? [[key, count]] : [],
|
|
),
|
|
);
|
|
}
|
|
|
|
function sanitizeFixtureState(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
return Object.fromEntries(
|
|
Object.entries(value).flatMap(([windowId, state]) => {
|
|
if (!safeId(windowId) || !state || typeof state !== 'object' || Array.isArray(state)) {
|
|
return [];
|
|
}
|
|
const safeState = Object.fromEntries(
|
|
Object.entries(state).flatMap(([key, field]) =>
|
|
safeId(key) &&
|
|
(typeof field === 'boolean' ||
|
|
typeof field === 'number' ||
|
|
(typeof field === 'string' && /^[A-Za-z0-9 .:_-]{0,128}$/.test(field)))
|
|
? [[key, field]]
|
|
: [],
|
|
),
|
|
);
|
|
return [[windowId, safeState]];
|
|
}),
|
|
);
|
|
}
|
|
|
|
function sanitizeAssertionResult(value) {
|
|
if (!value || typeof value !== 'object') return null;
|
|
const windowId = safeId(value.windowId);
|
|
const path = safeId(value.path);
|
|
if (!windowId || !path || typeof value.pass !== 'boolean') return null;
|
|
return {
|
|
windowId,
|
|
path,
|
|
pass: value.pass,
|
|
...(typeof value.actual === 'boolean' ||
|
|
typeof value.actual === 'number' ||
|
|
typeof value.actual === 'string'
|
|
? { actual: value.actual }
|
|
: {}),
|
|
};
|
|
}
|