864 lines
81 KiB
JavaScript
864 lines
81 KiB
JavaScript
function escapeHtml(value) {
|
||
return String(value)
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
const p = (text) => '<p>' + text + '</p>';
|
||
const h2 = (text) => '<h2>' + text + '</h2>';
|
||
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
|
||
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
|
||
|
||
function plainText(content) {
|
||
return content
|
||
.replace(/<[^>]+>/g, ' ')
|
||
.replaceAll(' ', ' ')
|
||
.replaceAll('"', '"')
|
||
.replaceAll(''', "'")
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('&', '&')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function bodyText(content) {
|
||
return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
|
||
}
|
||
|
||
const sources = Object.freeze([
|
||
Object.freeze({
|
||
title: 'GitHub Docs: Responsible use of GitHub Copilot Chat in GitHub',
|
||
url: 'https://github.com/github/docs/blob/0d17a12af77011b390abd8aea9e8626e153e0522/content/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-chat-in-github.md',
|
||
version: 'immutable GitHub Docs commit 0d17a12af77011b390abd8aea9e8626e153e0522, 30 April 2025',
|
||
claim: 'На закреплённой ревизии GitHub описывает, что prompt предварительно обрабатывается и сочетается с контекстом; модель может получать дополнительный repository context, а при включённом Bing query строится из prompt и доступного context и передаётся Bing Search API.',
|
||
boundary: 'Это описание конкретной поверхности Copilot Chat на указанной ревизии. Оно не устанавливает класс данных, срок retention, обучение модели, договор конкретной организации или разрешение сотруднику передавать фрагмент.',
|
||
}),
|
||
Object.freeze({
|
||
title: 'GitHub Docs reusable: Copilot SKU isolation',
|
||
url: 'https://github.com/github/docs/blob/0d17a12af77011b390abd8aea9e8626e153e0522/data/reusables/copilot/sku-isolation.md',
|
||
version: 'immutable GitHub Docs commit 0d17a12af77011b390abd8aea9e8626e153e0522, 30 April 2025',
|
||
claim: 'Документ описывает, что firewall allow-list плановых endpoint может разрешать Business или Enterprise endpoint и block-list может запрещать Individual endpoint внутри корпоративной сети.',
|
||
boundary: 'SKU isolation относится к маршрутизации и выбору plan endpoint. Он не показывает payload, не проверяет data class, не подтверждает retention или training terms и не заменяет human authorization.',
|
||
}),
|
||
Object.freeze({
|
||
title: 'NIST SP 800-207: Zero Trust Architecture',
|
||
url: 'https://csrc.nist.gov/pubs/sp/800/207/final',
|
||
version: 'NIST SP 800-207, final publication 11 August 2020',
|
||
claim: 'NIST не считает network location основанием для неявного доверия и описывает authentication и authorization как отдельные функции перед доступом к enterprise resource; доступ определяется policy для конкретного resource.',
|
||
boundary: 'Публикация не даёт vendor DPA, список AI endpoint, схему классификации или доказательство того, что конкретный внешний сервис не хранит и не использует переданные данные.',
|
||
}),
|
||
Object.freeze({
|
||
title: 'NIST AI 100-1: Artificial Intelligence Risk Management Framework 1.0',
|
||
url: 'https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf',
|
||
version: 'NIST AI 100-1, January 2023 final PDF',
|
||
claim: 'AI RMF организует управление риском вокруг GOVERN, MAP, MEASURE и MANAGE и рассматривает privacy вместе с организационными и техническими практиками, а не как одну настройку продукта.',
|
||
boundary: 'AI RMF — добровольная рамка управления риском. Она не создаёт юридический вывод, не классифицирует запись за команду и не заменяет договор, техническое измерение egress или полномочия владельца.',
|
||
}),
|
||
]);
|
||
|
||
export const sourceReport = Object.freeze(sources.map((source) => Object.freeze({ ...source })));
|
||
|
||
function sourceList() {
|
||
return '<ul>' + sources.map((source) => '<li><a href="' + source.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(source.title) + '</a> <em>(' + escapeHtml(source.version) + ')</em> — ' + escapeHtml(source.claim) + ' Ограничение: ' + escapeHtml(source.boundary) + '</li>').join('') + '</ul>';
|
||
}
|
||
|
||
function revision(meta, parts) {
|
||
const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + '\n' + sourceList();
|
||
const proseLength = bodyText(contentHtml).length;
|
||
if (proseLength < 5000 || proseLength > 15000) {
|
||
throw new Error(meta.slug + ': основной текст вне диапазона 5 000–15 000 знаков: ' + proseLength);
|
||
}
|
||
return Object.freeze({ ...meta, contentHtml, proseLength });
|
||
}
|
||
|
||
const MODEL_VERSION = 'fixed-synthetic-ai-data-privacy-2025-04-v1';
|
||
const FIXTURE_SCOPE = 'p86-ai-data-privacy-2025-04';
|
||
const MODEL_LIMIT = 'versioned fixed synthetic JavaScript literals in memory only; no customer code, PII, secret, prompt, model call, external request, file, Git, CI, network, telemetry, clock, production or side effect';
|
||
const CONTEXT_KIND = 'fixed-synthetic-ai-context-record/v1';
|
||
const REQUEST_KIND = 'fixed-synthetic-ai-egress-request/v1';
|
||
const AUTHORIZATION_KIND = 'fixed-synthetic-ai-human-authorization/v1';
|
||
const DECISION_KIND = 'fixed-synthetic-ai-egress-decision/v1';
|
||
const STOP_KIND = 'fixed-synthetic-ai-egress-stop/v1';
|
||
|
||
const CONTEXT_KEYS = Object.freeze(['kind', 'recordId', 'contextVersion', 'dataClass', 'allowability', 'retentionLabel', 'egressScope', 'authorityRequired', 'accessLabels', 'expiresAt', 'citation', 'payload']);
|
||
const REQUEST_KEYS = Object.freeze(['kind', 'caseId', 'syntheticOnly', 'modelVersion', 'requestId', 'requestedAt', 'requesterLabels', 'recordIds', 'requestedEgress', 'authorization', 'citationRequirement']);
|
||
const AUTHORIZATION_KEYS = Object.freeze(['kind', 'authorizationId', 'syntheticOnly', 'authorityRole', 'scopeRecordIds', 'egressScope', 'validFrom', 'expiresAt', 'requesterLabels', 'decision', 'reason', 'citation']);
|
||
const DECISION_KEYS = Object.freeze(['kind', 'accepted', 'syntheticOnly', 'modelVersion', 'requestProof', 'reason', 'citation', 'recordIds', 'classificationDecision', 'contractDecision', 'egressGate', 'authorizationDecision', 'stopCondition', 'nextAction', 'egressPerformed', 'modelLimit', 'productionEffect']);
|
||
const STOP_KEYS = Object.freeze(['kind', 'stopped', 'syntheticOnly', 'modelVersion', 'sourceDecisionProof', 'reason', 'citation', 'stopCondition', 'nextAction', 'egressPerformed', 'modelLimit', 'productionEffect']);
|
||
|
||
function isPlainRecord(value) {
|
||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||
const prototype = Object.getPrototypeOf(value);
|
||
return prototype === Object.prototype || prototype === null;
|
||
}
|
||
|
||
function hasExactKeys(value, expected) {
|
||
if (!isPlainRecord(value) || Object.getOwnPropertySymbols(value).length > 0) return false;
|
||
const keys = Object.keys(value);
|
||
return keys.length === expected.length
|
||
&& expected.every((key) => Object.hasOwn(value, key))
|
||
&& keys.every((key) => expected.includes(key));
|
||
}
|
||
|
||
function hasDenseArray(value) {
|
||
if (!Array.isArray(value)) return false;
|
||
const ownKeys = Reflect.ownKeys(value);
|
||
return ownKeys.length === value.length + 1
|
||
&& ownKeys.includes('length')
|
||
&& Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)).every(Boolean);
|
||
}
|
||
|
||
export function canonicalJson(value) {
|
||
const visited = new WeakSet();
|
||
|
||
function visit(current) {
|
||
if (current === null) return 'null';
|
||
if (typeof current === 'string' || typeof current === 'boolean') return JSON.stringify(current);
|
||
if (typeof current === 'number') return Number.isFinite(current) ? JSON.stringify(current) : null;
|
||
|
||
if (Array.isArray(current)) {
|
||
if (!hasDenseArray(current) || visited.has(current)) return null;
|
||
visited.add(current);
|
||
const values = current.map((item) => visit(item));
|
||
visited.delete(current);
|
||
return values.every((item) => item !== null) ? '[' + values.join(',') + ']' : null;
|
||
}
|
||
|
||
if (!isPlainRecord(current) || visited.has(current)) return null;
|
||
visited.add(current);
|
||
const values = Object.keys(current).sort().map((key) => {
|
||
const nested = visit(current[key]);
|
||
return nested === null ? null : JSON.stringify(key) + ':' + nested;
|
||
});
|
||
visited.delete(current);
|
||
return values.every((item) => item !== null) ? '{' + values.join(',') + '}' : null;
|
||
}
|
||
|
||
return visit(value);
|
||
}
|
||
|
||
export function hasSameCanonicalJson(left, right) {
|
||
const leftJson = canonicalJson(left);
|
||
const rightJson = canonicalJson(right);
|
||
return leftJson !== null && rightJson !== null && leftJson === rightJson;
|
||
}
|
||
|
||
function deepFreeze(value, seen = new WeakSet()) {
|
||
if (!value || typeof value !== 'object' || seen.has(value)) return value;
|
||
seen.add(value);
|
||
for (const child of Object.values(value)) deepFreeze(child, seen);
|
||
return Object.freeze(value);
|
||
}
|
||
|
||
function cloneFixed(value) {
|
||
const json = canonicalJson(value);
|
||
if (json === null) throw new Error('fixed synthetic value must be canonical');
|
||
return JSON.parse(json);
|
||
}
|
||
|
||
function sameIsoOrBefore(left, right) {
|
||
return typeof left === 'string' && typeof right === 'string' && left <= right;
|
||
}
|
||
|
||
function labelsCover(actual, required) {
|
||
return hasDenseArray(actual)
|
||
&& hasDenseArray(required)
|
||
&& required.every((label) => typeof label === 'string' && actual.includes(label));
|
||
}
|
||
|
||
const fixedContexts = deepFreeze({
|
||
'synthetic-public-interface-summary-v1': {
|
||
kind: CONTEXT_KIND,
|
||
recordId: 'synthetic-public-interface-summary-v1',
|
||
contextVersion: 'v1',
|
||
dataClass: 'synthetic-public',
|
||
allowability: 'allow-after-human-authorization',
|
||
retentionLabel: 'synthetic-contract-retention-reviewed',
|
||
egressScope: 'fixed-external-ai-boundary-alpha',
|
||
authorityRequired: 'synthetic-data-steward',
|
||
accessLabels: ['synthetic-engineering-read'],
|
||
expiresAt: '2025-04-30T23:59:59Z',
|
||
citation: 'synthetic://p86/context/public-interface-summary-v1',
|
||
payload: 'Synthetic interface summary: currency is a documented label; no customer, secret, source file or external identifier is present.',
|
||
},
|
||
'synthetic-internal-design-note-v2': {
|
||
kind: CONTEXT_KIND,
|
||
recordId: 'synthetic-internal-design-note-v2',
|
||
contextVersion: 'v2',
|
||
dataClass: 'synthetic-internal',
|
||
allowability: 'review-before-egress',
|
||
retentionLabel: 'synthetic-contract-not-yet-attached',
|
||
egressScope: 'fixed-external-ai-boundary-alpha',
|
||
authorityRequired: 'synthetic-data-steward',
|
||
accessLabels: ['synthetic-engineering-read'],
|
||
expiresAt: '2025-04-30T23:59:59Z',
|
||
citation: 'synthetic://p86/context/internal-design-note-v2',
|
||
payload: 'Synthetic design note: the record exists only to exercise the policy-review stop; it contains no repository or customer material.',
|
||
},
|
||
'synthetic-restricted-log-shape-v1': {
|
||
kind: CONTEXT_KIND,
|
||
recordId: 'synthetic-restricted-log-shape-v1',
|
||
contextVersion: 'v1',
|
||
dataClass: 'synthetic-restricted',
|
||
allowability: 'deny-external-egress',
|
||
retentionLabel: 'synthetic-no-external-retention',
|
||
egressScope: 'fixed-external-ai-boundary-alpha',
|
||
authorityRequired: 'synthetic-security-owner',
|
||
accessLabels: ['synthetic-security-read'],
|
||
expiresAt: '2025-04-30T23:59:59Z',
|
||
citation: 'synthetic://p86/context/restricted-log-shape-v1',
|
||
payload: 'Synthetic log shape: actor=[symbolic label], token=[not-present], request=[not-present]; this string intentionally contains no log event or personal data.',
|
||
},
|
||
'synthetic-ticket-shape-v1': {
|
||
kind: CONTEXT_KIND,
|
||
recordId: 'synthetic-ticket-shape-v1',
|
||
contextVersion: 'v1',
|
||
dataClass: 'synthetic-personal-data-shape',
|
||
allowability: 'deny-external-egress',
|
||
retentionLabel: 'synthetic-no-external-retention',
|
||
egressScope: 'fixed-external-ai-boundary-alpha',
|
||
authorityRequired: 'synthetic-privacy-owner',
|
||
accessLabels: ['synthetic-privacy-read'],
|
||
expiresAt: '2025-04-30T23:59:59Z',
|
||
citation: 'synthetic://p86/context/ticket-shape-v1',
|
||
payload: 'Synthetic ticket shape: requester=[not-present], contact=[not-present], narrative=[placeholder]; no ticket or person is represented.',
|
||
},
|
||
});
|
||
|
||
const fixedAuthorizations = deepFreeze({
|
||
'synthetic-public-summary-approval-v1': {
|
||
kind: AUTHORIZATION_KIND,
|
||
authorizationId: 'synthetic-public-summary-approval-v1',
|
||
syntheticOnly: true,
|
||
authorityRole: 'synthetic-data-steward',
|
||
scopeRecordIds: ['synthetic-public-interface-summary-v1'],
|
||
egressScope: 'fixed-external-ai-boundary-alpha',
|
||
validFrom: '2025-04-01T00:00:00Z',
|
||
expiresAt: '2025-04-20T00:00:00Z',
|
||
requesterLabels: ['synthetic-engineering-read'],
|
||
decision: 'allow-one-fixed-public-summary-only',
|
||
reason: 'fixed training authorization: one public synthetic record, one symbolic egress scope, no external request',
|
||
citation: 'synthetic://p86/authorization/public-summary-v1',
|
||
},
|
||
'synthetic-expired-public-summary-approval-v1': {
|
||
kind: AUTHORIZATION_KIND,
|
||
authorizationId: 'synthetic-expired-public-summary-approval-v1',
|
||
syntheticOnly: true,
|
||
authorityRole: 'synthetic-data-steward',
|
||
scopeRecordIds: ['synthetic-public-interface-summary-v1'],
|
||
egressScope: 'fixed-external-ai-boundary-alpha',
|
||
validFrom: '2025-04-01T00:00:00Z',
|
||
expiresAt: '2025-04-10T00:00:00Z',
|
||
requesterLabels: ['synthetic-engineering-read'],
|
||
decision: 'allow-one-fixed-public-summary-only',
|
||
reason: 'fixed expired authorization used only to test the expiry stop',
|
||
citation: 'synthetic://p86/authorization/expired-public-summary-v1',
|
||
},
|
||
});
|
||
|
||
const fixedCases = deepFreeze({
|
||
'authorized-public-summary': {
|
||
request: {
|
||
kind: REQUEST_KIND,
|
||
caseId: 'authorized-public-summary',
|
||
syntheticOnly: true,
|
||
modelVersion: MODEL_VERSION,
|
||
requestId: 'synthetic-request-public-summary-v1',
|
||
requestedAt: '2025-04-16T10:00:00Z',
|
||
requesterLabels: ['synthetic-engineering-read'],
|
||
recordIds: ['synthetic-public-interface-summary-v1'],
|
||
requestedEgress: 'fixed-external-ai-boundary-alpha',
|
||
authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'],
|
||
citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required',
|
||
},
|
||
},
|
||
'internal-policy-review-stop': {
|
||
request: {
|
||
kind: REQUEST_KIND,
|
||
caseId: 'internal-policy-review-stop',
|
||
syntheticOnly: true,
|
||
modelVersion: MODEL_VERSION,
|
||
requestId: 'synthetic-request-internal-note-v1',
|
||
requestedAt: '2025-04-16T10:00:00Z',
|
||
requesterLabels: ['synthetic-engineering-read'],
|
||
recordIds: ['synthetic-internal-design-note-v2'],
|
||
requestedEgress: 'fixed-external-ai-boundary-alpha',
|
||
authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'],
|
||
citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required',
|
||
},
|
||
},
|
||
'restricted-class-stop': {
|
||
request: {
|
||
kind: REQUEST_KIND,
|
||
caseId: 'restricted-class-stop',
|
||
syntheticOnly: true,
|
||
modelVersion: MODEL_VERSION,
|
||
requestId: 'synthetic-request-restricted-log-v1',
|
||
requestedAt: '2025-04-16T10:00:00Z',
|
||
requesterLabels: ['synthetic-security-read'],
|
||
recordIds: ['synthetic-restricted-log-shape-v1'],
|
||
requestedEgress: 'fixed-external-ai-boundary-alpha',
|
||
authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'],
|
||
citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required',
|
||
},
|
||
},
|
||
'access-boundary-stop': {
|
||
request: {
|
||
kind: REQUEST_KIND,
|
||
caseId: 'access-boundary-stop',
|
||
syntheticOnly: true,
|
||
modelVersion: MODEL_VERSION,
|
||
requestId: 'synthetic-request-access-boundary-v1',
|
||
requestedAt: '2025-04-16T10:00:00Z',
|
||
requesterLabels: ['synthetic-observer-read'],
|
||
recordIds: ['synthetic-public-interface-summary-v1'],
|
||
requestedEgress: 'fixed-external-ai-boundary-alpha',
|
||
authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'],
|
||
citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required',
|
||
},
|
||
},
|
||
'expired-authorization-stop': {
|
||
request: {
|
||
kind: REQUEST_KIND,
|
||
caseId: 'expired-authorization-stop',
|
||
syntheticOnly: true,
|
||
modelVersion: MODEL_VERSION,
|
||
requestId: 'synthetic-request-expired-authorization-v1',
|
||
requestedAt: '2025-04-16T10:00:00Z',
|
||
requesterLabels: ['synthetic-engineering-read'],
|
||
recordIds: ['synthetic-public-interface-summary-v1'],
|
||
requestedEgress: 'fixed-external-ai-boundary-alpha',
|
||
authorization: fixedAuthorizations['synthetic-expired-public-summary-approval-v1'],
|
||
citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required',
|
||
},
|
||
},
|
||
'egress-boundary-stop': {
|
||
request: {
|
||
kind: REQUEST_KIND,
|
||
caseId: 'egress-boundary-stop',
|
||
syntheticOnly: true,
|
||
modelVersion: MODEL_VERSION,
|
||
requestId: 'synthetic-request-wrong-egress-v1',
|
||
requestedAt: '2025-04-16T10:00:00Z',
|
||
requesterLabels: ['synthetic-engineering-read'],
|
||
recordIds: ['synthetic-public-interface-summary-v1'],
|
||
requestedEgress: 'fixed-unapproved-egress-scope',
|
||
authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'],
|
||
citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required',
|
||
},
|
||
},
|
||
});
|
||
|
||
function decisionFor(request, values) {
|
||
return deepFreeze({
|
||
kind: DECISION_KIND,
|
||
accepted: values.accepted,
|
||
syntheticOnly: true,
|
||
modelVersion: MODEL_VERSION,
|
||
requestProof: canonicalJson(request) || 'invalid-fixed-request-proof',
|
||
reason: values.reason,
|
||
citation: values.citation,
|
||
recordIds: values.recordIds,
|
||
classificationDecision: values.classificationDecision,
|
||
contractDecision: values.contractDecision,
|
||
egressGate: values.egressGate,
|
||
authorizationDecision: values.authorizationDecision,
|
||
stopCondition: values.stopCondition,
|
||
nextAction: values.nextAction,
|
||
egressPerformed: false,
|
||
modelLimit: MODEL_LIMIT,
|
||
productionEffect: 'not-attempted',
|
||
});
|
||
}
|
||
|
||
function rejectedDecision(request, reason, citation = 'synthetic://p86/decision/invalid-request', stopCondition = 'fixed request validation did not establish a safe synthetic scope') {
|
||
return decisionFor(request, {
|
||
accepted: false,
|
||
reason,
|
||
citation,
|
||
recordIds: [],
|
||
classificationDecision: 'not-established',
|
||
contractDecision: 'not-established',
|
||
egressGate: 'closed',
|
||
authorizationDecision: 'not-established',
|
||
stopCondition,
|
||
nextAction: 'retain no payload and ask for a new fixed synthetic request with one named boundary',
|
||
});
|
||
}
|
||
|
||
function validateFixedRequest(request) {
|
||
if (!hasExactKeys(request, REQUEST_KEYS)) return { ok: false, reason: 'request-exact-contract-failed' };
|
||
if (request.kind !== REQUEST_KIND || request.syntheticOnly !== true || request.modelVersion !== MODEL_VERSION) {
|
||
return { ok: false, reason: 'request-version-or-synthetic-boundary-failed' };
|
||
}
|
||
if (!hasDenseArray(request.requesterLabels) || !hasDenseArray(request.recordIds)) {
|
||
return { ok: false, reason: 'request-array-density-failed' };
|
||
}
|
||
const fixed = fixedCases[request.caseId];
|
||
if (!fixed || !hasSameCanonicalJson(request, fixed.request)) {
|
||
return { ok: false, reason: 'request-proof-or-authorization-forged' };
|
||
}
|
||
return { ok: true, fixed };
|
||
}
|
||
|
||
function recordForId(recordId) {
|
||
const record = fixedContexts[recordId];
|
||
if (!record || !hasExactKeys(record, CONTEXT_KEYS) || !hasDenseArray(record.accessLabels)) return null;
|
||
return record;
|
||
}
|
||
|
||
function authorizationIsWellFormed(authorization) {
|
||
return hasExactKeys(authorization, AUTHORIZATION_KEYS)
|
||
&& authorization.kind === AUTHORIZATION_KIND
|
||
&& authorization.syntheticOnly === true
|
||
&& hasDenseArray(authorization.scopeRecordIds)
|
||
&& hasDenseArray(authorization.requesterLabels);
|
||
}
|
||
|
||
function authorizationCovers(request, records) {
|
||
const authorization = request.authorization;
|
||
if (!authorizationIsWellFormed(authorization)) return { ok: false, reason: 'human-authorization-shape-failed' };
|
||
if (authorization.egressScope !== request.requestedEgress) return { ok: false, reason: 'authorization-egress-scope-mismatch' };
|
||
if (!sameIsoOrBefore(authorization.validFrom, request.requestedAt) || !sameIsoOrBefore(request.requestedAt, authorization.expiresAt)) {
|
||
return { ok: false, reason: 'human-authorization-expired-or-not-yet-valid' };
|
||
}
|
||
if (!labelsCover(request.requesterLabels, authorization.requesterLabels)) return { ok: false, reason: 'requester-access-does-not-match-authorization' };
|
||
if (!records.every((record) => authorization.scopeRecordIds.includes(record.recordId))) return { ok: false, reason: 'human-authorization-does-not-cover-record' };
|
||
if (!records.every((record) => record.authorityRequired === authorization.authorityRole)) return { ok: false, reason: 'human-authorization-owner-mismatch' };
|
||
if (authorization.decision !== 'allow-one-fixed-public-summary-only') return { ok: false, reason: 'human-authorization-decision-not-fixed' };
|
||
return { ok: true };
|
||
}
|
||
|
||
export function assessSyntheticAiEgress(request) {
|
||
const checked = validateFixedRequest(request);
|
||
if (!checked.ok) return rejectedDecision(request, checked.reason);
|
||
|
||
const records = request.recordIds.map(recordForId);
|
||
if (records.some((record) => record === null)) {
|
||
return rejectedDecision(request, 'record-id-is-not-a-fixed-versioned-context', 'synthetic://p86/decision/record-boundary');
|
||
}
|
||
|
||
const citation = records.map((record) => record.citation).join(' | ');
|
||
const classesAllow = records.every((record) => record.allowability === 'allow-after-human-authorization');
|
||
if (!classesAllow) {
|
||
return decisionFor(request, {
|
||
accepted: false,
|
||
reason: 'data class or allowability denies this synthetic external boundary',
|
||
citation,
|
||
recordIds: request.recordIds,
|
||
classificationDecision: 'denied-by-fixed-class-or-allowability',
|
||
contractDecision: 'not-accepted',
|
||
egressGate: 'closed',
|
||
authorizationDecision: 'not-consulted-as-an-override',
|
||
stopCondition: 'a restricted or review-required record cannot be promoted by this model',
|
||
nextAction: 'keep the record in memory, record its class and ask the owner for a policy/contract decision outside this package',
|
||
});
|
||
}
|
||
|
||
const retentionReviewed = records.every((record) => record.retentionLabel === 'synthetic-contract-retention-reviewed');
|
||
if (!retentionReviewed) {
|
||
return decisionFor(request, {
|
||
accepted: false,
|
||
reason: 'retention or contract label is not established for the fixed context',
|
||
citation,
|
||
recordIds: request.recordIds,
|
||
classificationDecision: 'class-permits-conditional-review',
|
||
contractDecision: 'retention-label-missing-or-denied',
|
||
egressGate: 'closed',
|
||
authorizationDecision: 'not-consulted-as-an-override',
|
||
stopCondition: 'a class label does not prove the required contract or retention condition',
|
||
nextAction: 'stop and obtain a separately checkable policy/contract record; do not infer it from a product setting',
|
||
});
|
||
}
|
||
|
||
const egressMatches = records.every((record) => record.egressScope === request.requestedEgress);
|
||
if (!egressMatches) {
|
||
return decisionFor(request, {
|
||
accepted: false,
|
||
reason: 'requested egress scope differs from the fixed context boundary',
|
||
citation,
|
||
recordIds: request.recordIds,
|
||
classificationDecision: 'class-permits-conditional-review',
|
||
contractDecision: 'retention-label-present',
|
||
egressGate: 'closed',
|
||
authorizationDecision: 'not-consulted-as-an-override',
|
||
stopCondition: 'destination or route is outside the record-specific synthetic scope',
|
||
nextAction: 'keep the context local and verify the destination with the network and service owners before a new authorized review',
|
||
});
|
||
}
|
||
|
||
const accessMatches = records.every((record) => labelsCover(request.requesterLabels, record.accessLabels));
|
||
if (!accessMatches) {
|
||
return decisionFor(request, {
|
||
accepted: false,
|
||
reason: 'requester access labels do not cover the fixed context record',
|
||
citation,
|
||
recordIds: request.recordIds,
|
||
classificationDecision: 'class-permits-conditional-review',
|
||
contractDecision: 'retention-label-present',
|
||
egressGate: 'scope-matches-but-closed',
|
||
authorizationDecision: 'requester-access-mismatch',
|
||
stopCondition: 'reading or handling access is not established for this requester',
|
||
nextAction: 'safe stop; ask the access owner to review the named record and requester scope, without copying its payload',
|
||
});
|
||
}
|
||
|
||
const recordsFresh = records.every((record) => sameIsoOrBefore(request.requestedAt, record.expiresAt));
|
||
if (!recordsFresh) {
|
||
return decisionFor(request, {
|
||
accepted: false,
|
||
reason: 'fixed context record is outside its review expiry boundary',
|
||
citation,
|
||
recordIds: request.recordIds,
|
||
classificationDecision: 'class-permits-conditional-review',
|
||
contractDecision: 'expired-context-record',
|
||
egressGate: 'closed',
|
||
authorizationDecision: 'not-accepted',
|
||
stopCondition: 'a versioned record past its fixed expiry cannot support hand-off',
|
||
nextAction: 'refresh the local classification and contract evidence with its owner; do not re-label the old record in place',
|
||
});
|
||
}
|
||
|
||
const authorization = authorizationCovers(request, records);
|
||
if (!authorization.ok) {
|
||
return decisionFor(request, {
|
||
accepted: false,
|
||
reason: authorization.reason,
|
||
citation,
|
||
recordIds: request.recordIds,
|
||
classificationDecision: 'class-permits-conditional-review',
|
||
contractDecision: 'retention-label-present',
|
||
egressGate: 'scope-matches-but-closed',
|
||
authorizationDecision: 'missing-expired-or-mismatched',
|
||
stopCondition: 'human authorization is scoped, dated and non-transferable in this fixed model',
|
||
nextAction: 'safe stop and request a fresh owner decision for the exact record, requester and destination',
|
||
});
|
||
}
|
||
|
||
return decisionFor(request, {
|
||
accepted: true,
|
||
reason: 'all fixed labels align for an authorized synthetic hand-off; no external request is performed',
|
||
citation: citation + ' | ' + request.authorization.citation,
|
||
recordIds: request.recordIds,
|
||
classificationDecision: 'fixed-class-permits-conditional-handoff',
|
||
contractDecision: 'fixed-retention-label-present',
|
||
egressGate: 'fixed-scope-matches',
|
||
authorizationDecision: 'fixed-human-authorization-valid-for-scope',
|
||
stopCondition: 'the model ends before any external request, payload transfer or production action',
|
||
nextAction: 'hand the named evidence to the real policy, network and data owners; collect fresh facts there before any actual transmission',
|
||
});
|
||
}
|
||
|
||
export function stopSyntheticAiEgress(decision) {
|
||
const recognized = hasExactKeys(decision, DECISION_KEYS)
|
||
&& decision.kind === DECISION_KIND
|
||
&& decision.syntheticOnly === true
|
||
&& decision.modelVersion === MODEL_VERSION
|
||
&& decision.egressPerformed === false
|
||
&& decision.productionEffect === 'not-attempted'
|
||
&& canonicalJson(decision) !== null;
|
||
const blocked = recognized ? !decision.accepted : true;
|
||
|
||
return deepFreeze({
|
||
kind: STOP_KIND,
|
||
stopped: true,
|
||
syntheticOnly: true,
|
||
modelVersion: MODEL_VERSION,
|
||
sourceDecisionProof: recognized ? canonicalJson(decision) : 'invalid-fixed-decision-proof',
|
||
reason: recognized
|
||
? (blocked ? decision.reason : 'model intentionally stops before any real egress')
|
||
: 'invalid decision cannot open an egress path',
|
||
citation: recognized ? decision.citation : 'synthetic://p86/stop/invalid-decision',
|
||
stopCondition: recognized ? decision.stopCondition : 'invalid decision lacks a fixed synthetic boundary',
|
||
nextAction: recognized ? decision.nextAction : 'discard the decision and create a new fixed in-memory request',
|
||
egressPerformed: false,
|
||
modelLimit: MODEL_LIMIT,
|
||
productionEffect: 'not-attempted',
|
||
});
|
||
}
|
||
|
||
export function createFixedSyntheticContextRequest(caseId = 'authorized-public-summary') {
|
||
const fixed = fixedCases[caseId];
|
||
if (!fixed) throw new Error('unknown fixed synthetic case: ' + caseId);
|
||
return cloneFixed(fixed.request);
|
||
}
|
||
|
||
export function runAiDataPrivacyFixture() {
|
||
const allowed = createFixedSyntheticContextRequest('authorized-public-summary');
|
||
const allowedDecision = assessSyntheticAiEgress(allowed);
|
||
const allowedStop = stopSyntheticAiEgress(allowedDecision);
|
||
|
||
const internal = createFixedSyntheticContextRequest('internal-policy-review-stop');
|
||
const internalDecision = assessSyntheticAiEgress(internal);
|
||
const restricted = createFixedSyntheticContextRequest('restricted-class-stop');
|
||
const restrictedDecision = assessSyntheticAiEgress(restricted);
|
||
const access = createFixedSyntheticContextRequest('access-boundary-stop');
|
||
const accessDecision = assessSyntheticAiEgress(access);
|
||
const expired = createFixedSyntheticContextRequest('expired-authorization-stop');
|
||
const expiredDecision = assessSyntheticAiEgress(expired);
|
||
const wrongEgress = createFixedSyntheticContextRequest('egress-boundary-stop');
|
||
const wrongEgressDecision = assessSyntheticAiEgress(wrongEgress);
|
||
|
||
const extraKey = { ...allowed, unexpected: true };
|
||
const missingKey = { ...allowed };
|
||
delete missingKey.requestId;
|
||
const forgedAuthorization = cloneFixed(allowed);
|
||
forgedAuthorization.authorization.decision = 'allow-all-synthetic-context';
|
||
const forgedScope = cloneFixed(allowed);
|
||
forgedScope.authorization.scopeRecordIds.push('synthetic-restricted-log-shape-v1');
|
||
const sparseRecordIds = cloneFixed(allowed);
|
||
delete sparseRecordIds.recordIds[0];
|
||
const sparseAuthorization = cloneFixed(allowed);
|
||
delete sparseAuthorization.authorization.scopeRecordIds[0];
|
||
const cyclicRequest = cloneFixed(allowed);
|
||
cyclicRequest.requesterLabels.push(cyclicRequest);
|
||
const cyclicAuthorization = cloneFixed(allowed);
|
||
cyclicAuthorization.authorization.scopeRecordIds.push(cyclicAuthorization.authorization);
|
||
const forgedDecision = { ...allowedDecision, accepted: true, egressPerformed: true };
|
||
|
||
const fixedBefore = canonicalJson(fixedContexts['synthetic-public-interface-summary-v1']);
|
||
assessSyntheticAiEgress(allowed);
|
||
const fixedAfter = canonicalJson(fixedContexts['synthetic-public-interface-summary-v1']);
|
||
|
||
const assertions = {
|
||
fixtureScopeIsFixed: FIXTURE_SCOPE === 'p86-ai-data-privacy-2025-04',
|
||
modelIsStrictlyMemoryOnly: MODEL_LIMIT.includes('in memory only') && MODEL_LIMIT.includes('no customer code') && MODEL_LIMIT.includes('network') && MODEL_LIMIT.includes('side effect'),
|
||
everyContextHasExactContract: Object.values(fixedContexts).every((record) => hasExactKeys(record, CONTEXT_KEYS)),
|
||
everyContextAccessListIsDense: Object.values(fixedContexts).every((record) => hasDenseArray(record.accessLabels)),
|
||
everyAuthorizationHasExactContract: Object.values(fixedAuthorizations).every((authorization) => hasExactKeys(authorization, AUTHORIZATION_KEYS)),
|
||
allowedRequestHasExactContract: hasExactKeys(allowed, REQUEST_KEYS),
|
||
allowedRequestHasDenseLists: hasDenseArray(allowed.requesterLabels) && hasDenseArray(allowed.recordIds) && hasDenseArray(allowed.authorization.scopeRecordIds),
|
||
allowedDecisionHasExactContract: hasExactKeys(allowedDecision, DECISION_KEYS),
|
||
allowedDecisionIsOnlyHandoff: allowedDecision.accepted === true && allowedDecision.egressPerformed === false && allowedDecision.productionEffect === 'not-attempted',
|
||
allowedDecisionSeparatesBoundaries: allowedDecision.classificationDecision === 'fixed-class-permits-conditional-handoff' && allowedDecision.contractDecision === 'fixed-retention-label-present' && allowedDecision.egressGate === 'fixed-scope-matches' && allowedDecision.authorizationDecision === 'fixed-human-authorization-valid-for-scope',
|
||
allowedPathStillStopsBeforeIO: allowedStop.stopped === true && allowedStop.egressPerformed === false && allowedStop.productionEffect === 'not-attempted',
|
||
internalPolicyGapStops: internalDecision.accepted === false && internalDecision.reason.includes('data class or allowability') && internalDecision.egressGate === 'closed',
|
||
restrictedClassStopsBeforeAuthorizationOverride: restrictedDecision.accepted === false && restrictedDecision.classificationDecision === 'denied-by-fixed-class-or-allowability' && restrictedDecision.authorizationDecision === 'not-consulted-as-an-override',
|
||
accessBoundaryStops: accessDecision.accepted === false && accessDecision.reason.includes('requester access') && accessDecision.egressGate.includes('closed'),
|
||
expiredAuthorizationStops: expiredDecision.accepted === false && expiredDecision.reason.includes('expired-or-not-yet-valid') && expiredDecision.authorizationDecision === 'missing-expired-or-mismatched',
|
||
egressBoundaryStops: wrongEgressDecision.accepted === false && wrongEgressDecision.reason.includes('egress scope differs') && wrongEgressDecision.egressGate === 'closed',
|
||
extraRequestKeyFailsClosed: assessSyntheticAiEgress(extraKey).accepted === false,
|
||
missingRequestKeyFailsClosed: assessSyntheticAiEgress(missingKey).accepted === false,
|
||
forgedAuthorizationFailsClosed: assessSyntheticAiEgress(forgedAuthorization).accepted === false,
|
||
forgedAuthorizationScopeFailsClosed: assessSyntheticAiEgress(forgedScope).accepted === false,
|
||
sparseRecordListFailsClosed: assessSyntheticAiEgress(sparseRecordIds).accepted === false,
|
||
sparseAuthorizationListFailsClosed: assessSyntheticAiEgress(sparseAuthorization).accepted === false,
|
||
cyclicRequestFailsClosedWithoutThrow: assessSyntheticAiEgress(cyclicRequest).accepted === false,
|
||
cyclicAuthorizationFailsClosedWithoutThrow: assessSyntheticAiEgress(cyclicAuthorization).accepted === false,
|
||
forgedDecisionStopsClosed: stopSyntheticAiEgress(forgedDecision).reason === 'invalid decision cannot open an egress path',
|
||
fixedContextWasNotMutated: fixedBefore === fixedAfter,
|
||
resultDoesNotExposePayload: !Object.hasOwn(allowedDecision, 'payload') && !Object.hasOwn(allowedStop, 'payload'),
|
||
unsupportedCaseThrowsBeforeAnyIO: (() => {
|
||
try {
|
||
createFixedSyntheticContextRequest('not-a-fixed-case');
|
||
return false;
|
||
} catch {
|
||
return true;
|
||
}
|
||
})(),
|
||
};
|
||
|
||
return deepFreeze({ kind: 'fixed-synthetic-ai-data-privacy-fixture/v1', assertions });
|
||
}
|
||
|
||
const practice = revision({
|
||
slug: 'editorial-2025-04-practice-ai-data-privacy',
|
||
title: 'Контекст без класса не должен уходить в AI-инструмент',
|
||
excerpt: 'Короткий маршрут, который отделяет класс данных, contract, egress и human authorization до передачи фрагмента в AI-инструмент.',
|
||
readingMinutes: 11,
|
||
}, [
|
||
p('Фрагмент кода, лога или тикета часто выглядит безобидно, пока его не нужно вставить в AI-инструмент. Но у фрагмента уже есть происхождение, класс, владелец и возможные получатели контекста. Симптом — инженер видит полезный вопрос и отправляет «маленький кусок», не зная, что к prompt может добавиться контекст продукта или поиска. Цена ошибки — не абстрактная приватность: команда теряет возможность объяснить, что именно ушло за границу, кто это разрешил и какой договорный режим должен был работать.'),
|
||
p('Причина обычно в смешении четырёх разных решений. Data classification отвечает, что представляет запись. Policy и contract отвечают, допускается ли такой класс и при каких retention или training conditions. Технический egress отвечает, куда может пойти трафик. Human authorization отвечает, кто разрешил именно этот scope и на какой срок. Если заменить все четыре вопроса одним «у нас есть DPA» или «в интерфейсе выключена галочка», следующее расследование начнётся без исходных доказательств.'),
|
||
h2('Передавать не текст, а описанную единицу контекста'),
|
||
p('Первый рабочий шаг — не редактировать prompt на глаз, а присвоить фрагменту versioned record. В нём нужны хотя бы record id, version, class, allowability, retention label, egress scope, required authority, access label, expiry и citation. Это не универсальная схема для права или закупки. Это инженерная карточка, благодаря которой человек может проверить ровно один фрагмент, а не спорить о слове «внутренний». Если значения неизвестны, карточка должна останавливаться, а не получать наиболее оптимистичный label.'),
|
||
p('Class — это не разрешение. Запись с class <code>synthetic-public</code> в учебной модели всё равно требует policy/contract label и отдельного разрешения владельца. И наоборот, даже корректный человек не получает права поднять <code>synthetic-restricted</code> до допустимого класса одной подписью. Такое разделение неприятно в первый день, потому что прибавляет несколько полей. Зато на следующем review не приходится угадывать, относим ли мы обсуждение к содержимому, маршруту или полномочиям.'),
|
||
figure('/assets/editorial/2025/ai-data-privacy-2025-context-boundary.svg', 'Пять независимых границ перед AI-инструментом: class данных, допустимость и retention, technical egress, human authorization и hand-off без реальной отправки.', 'Схема показывает, что отсутствие доказательства на любом шаге ведёт к safe stop. Зелёный hand-off означает только готовность передать вопрос владельцам, а не выполненный внешний запрос.'),
|
||
h2('Минимальная карточка: какие поля отвечают на какие вопросы'),
|
||
table('Поля fixed synthetic record', ['Поле', 'Проверяемый вопрос', 'Пример label', 'Чего поле не доказывает'], [
|
||
['dataClass', 'Какой локальный класс у содержимого?', 'synthetic-public или synthetic-restricted', 'условия vendor contract и сетевой маршрут'],
|
||
['allowability', 'Можно ли вообще рассматривать egress?', 'allow-after-human-authorization', 'что разрешение уже выдано конкретному requester'],
|
||
['retentionLabel', 'Есть ли проверенное условие хранения/обучения для нужного scope?', 'synthetic-contract-retention-reviewed', 'фактический payload, endpoint или будущую версию сервиса'],
|
||
['egressScope', 'Какой symbolic destination разрешён карточке?', 'fixed-external-ai-boundary-alpha', 'что firewall действительно пропустил данные или что payload безопасен'],
|
||
['authorityRequired и expiry', 'Кто и до какой даты вправе одобрить hand-off?', 'synthetic-data-steward до 20 апреля', 'права всех будущих пользователей или другой record'],
|
||
['citation', 'На какую карточку и решение можно сослаться?', 'synthetic://p86/…', 'доказательство реальной политики вне учебной модели'],
|
||
]),
|
||
p('В реальном процессе поля могут жить в каталоге данных, тикете, policy-as-code или системе заявок. Место не важно для этой статьи; важна связь. Если class записан в одном месте, contract в другом, egress в третьем, а approval лежит в личной переписке, review не может собрать их в один проверяемый chain. Тогда короткий prompt экономит минуту, но создаёт дорогой вопрос «почему мы решили, что это допустимо?».'),
|
||
h2('Воспроизводимый пример: только фиксированные записи в памяти'),
|
||
p('Ниже не показан запрос к модели и не передаётся даже синтетический payload. Скрипт создаёт заранее заданную карточку, проверяет равенство class, contract label, egress scope и dated authorization, а затем возвращает decision. Даже положительный outcome заканчивается на hand-off: поле <code>egressPerformed</code> остаётся <code>false</code>. Такая форма полезна для обучения review, потому что нельзя случайно выдать пример за интеграцию.'),
|
||
code([
|
||
"import {",
|
||
" createFixedSyntheticContextRequest,",
|
||
" assessSyntheticAiEgress,",
|
||
" stopSyntheticAiEgress,",
|
||
"} from './upgrade-2025-04.mjs';",
|
||
'',
|
||
"const request = createFixedSyntheticContextRequest('authorized-public-summary');",
|
||
'const decision = assessSyntheticAiEgress(request);',
|
||
'const boundaryStop = stopSyntheticAiEgress(decision);',
|
||
'',
|
||
'console.log(decision.accepted); // true: fixed hand-off only',
|
||
'console.log(decision.egressPerformed); // false',
|
||
'console.log(boundaryStop.stopped); // true: no external request exists',
|
||
].join('\n')),
|
||
p('У модели есть и отрицательные cases: internal record без settled contract label, restricted log shape, requester без нужного access label, expired authorization и другой egress scope. Каждый возвращает reason, citation, stop condition и next action. Важна формулировка stop: «на этом evidence нельзя открыть путь», а не «данные навсегда запрещены». Внешний мир может содержать дополнительные договоры и технические меры; эта модель просто не имеет права их придумывать.'),
|
||
h2('Как проводить короткий pre-flight review'),
|
||
ol([
|
||
'<strong>Назовите единицу.</strong> Запишите record id, version и class до того, как копировать фрагмент в prompt или подключать инструмент к репозиторию.',
|
||
'<strong>Разведите policy и contract.</strong> Сверьте allowability с нужным retention/training condition для данного service scope; не переносите вывод с другого plan или старой версии.',
|
||
'<strong>Проверьте egress отдельно.</strong> Сопоставьте destination, route и технический allow-list. Наличие сети или лицензии не делает payload допустимым.',
|
||
'<strong>Сверьте access и authority.</strong> Requester должен иметь доступ к record, а владелец — выдать scoped, dated решение. Approval без scope не переносится на соседний фрагмент.',
|
||
'<strong>Запишите safe stop.</strong> Если один факт неизвестен, оставьте payload локально, укажите owner question и не ищите обход через другой UI или account.',
|
||
]),
|
||
h2('Почему интерфейс и договор не закрывают весь маршрут'),
|
||
p('Фиксированный GitHub Docs на апрель 2025 показывает полезную инженерную деталь: prompt может обрабатываться вместе с контекстом, а отдельные возможности могут собирать repository context или сформировать Bing query при включённом поиске. Это не утверждение о каждом AI-инструменте. Оно показывает, почему проверять только видимый кусок текста недостаточно: нужно знать, какая поверхность и какие дополнительные источники контекста включены в данном режиме.'),
|
||
p('Другой GitHub документ описывает SKU isolation через plan-specific endpoint в firewall. Это хороший пример технического control: он может ограничить, какой plan endpoint используют люди в сети. Но он не видит data class, не сравнивает условие retention, не проверяет, что человек вправе раскрыть record, и не заменяет решение владельца. NIST SP 800-207 так же отделяет policy-based access от одного лишь network location. В статье это не юридическая консультация, а причина не считать один control доказательством всех остальных.'),
|
||
h2('Ограничения и следующий проверяемый шаг'),
|
||
p('Весь пример — versioned fixed synthetic JavaScript literals в памяти. Здесь нет customer code, PII, secret, реального лога, тикета, файла, Git, CI, telemetry, сети, clock, model call или production side effect. Labels <code>synthetic-public</code>, <code>synthetic-restricted</code> и <code>synthetic-contract-retention-reviewed</code> не описывают ни одну вашу систему и не доказывают фактическое хранение или обучение у поставщика. Они нужны только для проверки формы решения и fail-closed пути.'),
|
||
p('Следующий шаг: возьмите один тип контекста, который команда реально хочет использовать с AI, но не копируйте его в шаблон. Сначала создайте пустую карточку с class, allowability, contract/retention evidence, egress scope, owner, expiry и citation. Если хотя бы одно поле нельзя честно заполнить, результат review уже полезен: это safe stop и конкретный вопрос к data, security, legal или service owner.'),
|
||
h2('Историческая граница апреля 2025'),
|
||
p('Здесь используются только источники, существовавшие к 30 апреля 2025: два immutable GitHub Docs commit, NIST SP 800-207 final 2020 и NIST AI RMF 1.0 final 2023. В текст не перенесены поздние model names, provider terms или функции агентов. Любая будущая проверка должна заново закрепить версию документа и применимость к своему plan, region, contract и маршруту.'),
|
||
]);
|
||
|
||
const mechanism = revision({
|
||
slug: 'editorial-2025-04-mechanism-ai-data-privacy',
|
||
title: 'Четыре разных доказательства для AI-egress',
|
||
excerpt: 'Почему class, policy/contract, technical egress и human authorization должны сходиться на одной версии контекста, а не заменять друг друга.',
|
||
readingMinutes: 12,
|
||
}, [
|
||
p('Самая дорогая ошибка в работе с AI-контекстом часто происходит без утечки, сбоя или громкого инцидента. Команда уже не может восстановить решение: был ли фрагмент разрешён, к какому plan он относился, куда шёл трафик и кто согласовал срок. Цена ошибки — остановка внедрения, повторная инвентаризация и риск принять второе решение по памяти вместо фактов. Особенно плохо, когда evidence распределён между настройкой IDE, DPA, сетевым правилом и устным «можно».'),
|
||
p('Причина — попытка сделать один артефакт универсальным. Classification не равна policy, policy не равна contract, contract не равен egress, egress не равен authorization. Они пересекаются только в том, что все относятся к одной передаче. М8-автор не предлагает «самую строгую» кнопку: он строит короткую цепочку, в которой каждое звено отвечает на свой вопрос и честно оставляет границу видимой.'),
|
||
h2('Разложить решение на четыре оси'),
|
||
p('Data classification описывает содержимое локальным языком команды: public, internal, restricted, personal-data-shape или другой явно определённый class. Это вход в решение, а не вывод о поставщике. Policy задаёт допустимость: например, class может требовать отдельного review или прямо закрывать внешнюю передачу. Contract уточняет, какие обязательства проверены для выбранного service scope: retention, processing, training terms, region или subprocessor — ровно те, которые важны организации. Нельзя заменить неизвестное условие словом «корпоративный».'),
|
||
p('Technical egress отвечает уже на другой вопрос: есть ли путь к конкретному destination и соответствует ли он разрешённой конфигурации. Firewall allow-list, proxy rule, DNS policy или network segmentation полезны, но они работают с route и endpoint, а не со смыслом фрагмента. Human authorization отвечает на третий слой принятия решения: какой owner рассмотрел record, requester, destination и срок. Владелец не должен переопределять deny-class молча, а сеть не должна доказывать его полномочия.'),
|
||
figure('/assets/editorial/2025/ai-data-privacy-2025-allowability-matrix.svg', 'Матрица четырёх synthetic context record показывает, что class, policy/contract, egress и authority дают разные outcomes: hand-off только у одного fixed public case, остальные ведут в stop.', 'Матрица не классифицирует реальные данные. Она делает заметным, почему положительный факт в одной колонке не переносится в соседнюю.'),
|
||
h2('Матрица доказательств: какой артефакт имеет какой scope'),
|
||
table('Не путать proof с похожим сигналом', ['Вопрос', 'Нужное evidence', 'Что может помочь', 'Чего этого недостаточно доказать'], [
|
||
['Какой class у record?', 'версионированная карточка и owner vocabulary', 'название папки или тега', 'допустимость внешней передачи'],
|
||
['Допустим ли class?', 'policy decision и contract condition для service scope', 'DPA или UI setting как вход в review', 'фактический destination и применимость к конкретному plan'],
|
||
['Куда может пойти трафик?', 'проверенный destination, route и egress control', 'firewall allow-list', 'что payload соответствует class или что contract покрывает usage'],
|
||
['Кто разрешил передачу?', 'dated authorization с record, requester и scope', 'лицензия пользователя или одобрение продукта', 'права на другой record, срок или destination'],
|
||
['Что делать при пробеле?', 'reason, citation, stop condition и owner question', 'общее «надо уточнить»', 'обход через другой account, device или tool'],
|
||
]),
|
||
p('Это не бюрократическая таблица ради таблицы. Она сокращает конфликт review. Когда security говорит «endpoint разрешён», а privacy отвечает «record не классифицирован», оба могут быть правы, просто отвечают на разные вопросы. Правильное действие — не голосовать, чей control сильнее, а сохранить два факта и закрыть egress до тех пор, пока policy/contract факт не появится. Так stop остаётся техническим решением о неполном evidence, а не личной оценкой осторожности коллеги.'),
|
||
h2('Механизм fail-closed: сначала проверки, затем только hand-off'),
|
||
p('В P86 одинаково важны порядок и отрицательный результат. Модель сначала проверяет exact keys и canonical form request, потом вытаскивает только known fixed records. Далее она оценивает allowability и retention label, сравнивает egress scope, access label и датированную authorization. Нельзя начать с approval: иначе оно станет способом скрыть restricted class. Нельзя начать с network: иначе успешный route станет ложным доказательством допустимости.'),
|
||
code([
|
||
"const restricted = createFixedSyntheticContextRequest('restricted-class-stop');",
|
||
'const decision = assessSyntheticAiEgress(restricted);',
|
||
'const stop = stopSyntheticAiEgress(decision);',
|
||
'',
|
||
'console.log(decision.classificationDecision);',
|
||
'// denied-by-fixed-class-or-allowability',
|
||
'console.log(decision.authorizationDecision);',
|
||
'// not-consulted-as-an-override',
|
||
'console.log(stop.egressPerformed);',
|
||
'// false',
|
||
].join('\n')),
|
||
p('Низкий уровень реализации здесь нарочно скучный. Record ids, authorization и expected fields не принимаются «похожими». Лишний key, missing key, sparse array, cyclic value или forged authorization не получают частичный accept: fixture возвращает closed decision. Это защищает учебный пример от обычной ошибки автоматизации, когда сериализатор или удобный spread silently добавляет неоговорённую информацию. В production нужны другие controls, но fail-closed форма полезна уже до выбора tool.'),
|
||
h2('Retentions и training terms — отдельная проверка, не магический label'),
|
||
p('Retention label в примере называется <code>synthetic-contract-retention-reviewed</code>. Он не утверждает, что какой-либо vendor реально не хранит prompt, не обучается на нём или не передаёт его subprocessors. Он говорит гораздо уже: для одной synthetic карточки в учебной модели существует проверяемое условие, без которого hand-off закрыт. В реальной работе label должен ссылаться на документ и версию, applicable plan, region, accounts, features и исключения. Если термины меняются, изменяется не только ссылка, но и вывод.'),
|
||
p('Именно здесь полезна дисциплина immutable sources. GitHub Docs на закреплённом commit фиксирует, что конкретная chat surface может сочетать user input с дополнительным context и при включённом Bing отправлять сформированный query в Bing Search API. Это не доказывает retention или training для вашего продукта, но помогает задать правильный вопрос: какой контекст собирает именно этот surface и есть ли дополнительные outbound paths? Необходимо проверять не только marketing page, а технический режим, который включён команде.'),
|
||
h2('Egress control и zero trust: полезный, но ограниченный слой'),
|
||
p('GitHub SKU isolation описывает plan-specific endpoint и firewall allow/block list. Такой control может снизить риск того, что сотрудник в корпоративной сети использует не тот subscription path. Но данные не приобретают класс в момент TLS connection. Поэтому в таблице egress gate идёт после class и contract, а не вместо них. Если proxy разрешил host, остаются вопросы: какой feature включён, какой context он добавляет, соответствует ли record нужному scope и есть ли human authorization на этот date.'),
|
||
p('NIST SP 800-207 формулирует эту мысль шире: network location сама по себе не даёт implicit trust, а access к resource опирается на policy и отдельные authentication/authorization functions. В этой статье AI-инструмент не объявляется enterprise resource из NIST и не переносится схема стандарта один к одному. Важен принцип разделения: факт «путь существует» не отвечает на вопрос «данный субъект вправе передать данный record по этому пути».'),
|
||
h2('Порядок review, который можно повторить'),
|
||
ol([
|
||
'<strong>Соберите одну строку identity.</strong> Record id, version, class и owner должны быть видны до обсуждения prompt.',
|
||
'<strong>Спросите policy/contract question.</strong> Какое условие допустимости и retention/training evidence нужно именно для этой surface, plan и destination?',
|
||
'<strong>Спросите egress question.</strong> Какая техническая граница разрешена и кто подтвердил её применимость, не подменяя этим class?',
|
||
'<strong>Спросите authority question.</strong> Кого достаточно для решения, какой requester и date покрыты, а какие нет?',
|
||
'<strong>Сделайте один outcome.</strong> Hand-off в отдельный authorised workflow или safe stop с reason, citation и следующим владельцем. Не делайте send в ходе review.',
|
||
]),
|
||
h2('Ограничения и следующий проверяемый шаг'),
|
||
p('Ни матрица, ни fixture не являются privacy impact assessment, договором или архитектурой DLP. Они не обнаруживают PII, не проверяют endpoint, не читают logs, не знают region, не вызывают model и не оценивают legal basis. В фиксированных strings намеренно нет customer data, secret, исходного кода, реального тикета или telemetry. Поэтому положительный synthetic outcome нельзя превращать в разрешение на любой настоящий prompt, а отрицательный нельзя использовать как общий запрет на технологию.'),
|
||
p('Следующий шаг: для одного включённого AI feature создайте four-column evidence sheet: class/owner, policy+contract, egress, authorization. В каждой колонке оставьте URL или immutable reference, date и boundary: что этот факт не доказывает. Затем возьмите один известный gap и проверьте, что система безопасно останавливается без доступа к payload. Результат должен быть короче policy memo, но достаточен, чтобы другой reviewer повторил вопрос.'),
|
||
h2('Историческая граница апреля 2025'),
|
||
p('Пакет опирается на GitHub Docs commit от 30 апреля 2025 и на финальные NIST публикации 2020 и 2023. Он не предполагает, что поведение, storage, model routing или product terms позднее не изменятся. При реальном внедрении документация поставщика, договор и конфигурация проверяются заново на дату решения; эта статья показывает форму проверки, а не переносимый verdict.'),
|
||
]);
|
||
|
||
const field = revision({
|
||
slug: 'editorial-2025-04-field-ai-data-privacy',
|
||
title: 'Когда synthetic log shape должен остановить AI-review',
|
||
excerpt: 'Кейс без реальных логов и PII: как пройти class, contract, egress, access и expiry до того, как AI-инструмент увидит контекст.',
|
||
readingMinutes: 12,
|
||
}, [
|
||
p('Во время разбора ошибки инженеру хочется показать AI-инструменту «всего один лог и один тикет»: так легче получить гипотезу о причине. Но если у фрагмента нет class, нельзя сказать, допустим ли сам вопрос, а не только удобен ли ответ. Цена ошибки — потеря control над цепочкой: в следующем review никто не отличит разрешённый public example от restricted event shape, не увидит destination и не сможет доказать, что approval действовал в тот момент.'),
|
||
p('В этом кейсе нет настоящего лога, клиента, идентификатора, секретного значения, файла или сетевого запроса. Есть только fixed synthetic records с нарочно скучными placeholder. Это важно: пример учит остановить передачу до egress, а не демонстрирует «безопасный способ» отправить production context. Симптом — кто-то хочет дать ассистенту debugging material. Причина — четыре решения слились в просьбу «разрешите prompt». Проверка и действие разложены дальше по шагам.'),
|
||
h2('Case 1. Class звучит как «log», но decision зависит от содержимого и rule'),
|
||
p('Первый record называется <code>synthetic-restricted-log-shape-v1</code>. Его payload не содержит события: это строка с symbolic labels <code>actor=[symbolic label]</code>, <code>token=[not-present]</code> и <code>request=[not-present]</code>. Всё равно record получает class <code>synthetic-restricted</code>, allowability <code>deny-external-egress</code> и retention label <code>synthetic-no-external-retention</code>. Название «shape» не пытается спрятать риск; наоборот, заставляет review решить, нужен ли реальный schema owner до обсуждения материала.'),
|
||
p('Симптом: «мы же удалили значения, значит фрагмент можно передать». Причина: redaction, class и contract — разные операции. Проверка: спросить, кто утвердил class именно после преобразования, что осталось в structure, какие условия обработки применимы и есть ли evidence для конкретного tool surface. Действие: если class или allowability не доказаны, хранить фрагмент локально и открыть owner question. Нельзя поднять deny-class тем, что reviewer уверен в хороших намерениях автора.'),
|
||
h2('Case 2. Egress gate закрыт даже при знакомом продукте'),
|
||
p('В model <code>egressScope</code> — символическая строка <code>fixed-external-ai-boundary-alpha</code>. Она не является hostname, proxy configuration или реальным endpoint. Её задача — показать правило сопоставления: record, request и authorization должны назвать один scope. Если request просит <code>fixed-unapproved-egress-scope</code>, decision закрывается до проверки human authorization. Это предотвращает подмену «у человека есть approval» на «он может выбрать любой путь».'),
|
||
p('Симптом: команда знает, что продукт закуплен, и поэтому считает любой его UI одинаковой границей. Причина: plan, surface, подключённый search, extension и route могут иметь разный context flow. Проверка: выписать destination как отдельную сущность и найти техническое evidence для этого режима. Действие: если destination не совпал с record scope, вернуть safe stop с reason и citation. Не искать удобный альтернативный account или device: он меняет путь, но не закрывает policy gap.'),
|
||
figure('/assets/editorial/2025/ai-data-privacy-2025-egress-review.svg', 'Вертикальный маршрут review: record/class, policy/contract, egress, access/authorization и decision с citation; любое mismatch ведёт в safe stop без передачи наружу.', 'Последовательность отделяет evidence от действия. Даже зелёный hand-off остаётся внутри fixed synthetic model и не выполняет external request.'),
|
||
h2('Case 3. Authorization имеет scope, access и expiry'),
|
||
p('В положительном fixed case approval принадлежит роли <code>synthetic-data-steward</code>, покрывает только <code>synthetic-public-interface-summary-v1</code>, только <code>fixed-external-ai-boundary-alpha</code>, requester label <code>synthetic-engineering-read</code> и заканчивается 20 апреля 2025. Он не покрывает restricted log shape и не действует после срока. Такая точность может казаться излишней для одного фрагмента, но без неё authorization становится переносимой печатью «можно AI».'),
|
||
p('Симптом: у человека есть старое одобрение, но никто не сверил requester и date. Причина: approval считают свойством tool или команды, а не решением для конкретного resource. Проверка: сопоставить record ids, destination, requester labels, issuer role, validFrom и expiresAt. Действие: при любом mismatch model возвращает safe stop; новый owner должен принять новое решение на свежем evidence. Не изменяйте expiry в старой карточке задним числом: это убирает историю, которая нужна следующему reviewer.'),
|
||
table('Review record для одного synthetic request', ['Шаг', 'Наблюдение', 'Decision в модели', 'Следующее действие'], [
|
||
['Class', 'log-shape имеет synthetic-restricted и deny-external-egress', 'classification denied', 'не передавать payload; спросить owner о допустимой учебной замене'],
|
||
['Contract', 'retention label — synthetic-no-external-retention', 'contract not accepted', 'не выводить условия vendor по аналогии с другим plan'],
|
||
['Egress', 'scope request не совпадает с record', 'gate closed', 'уточнить destination/route у network и service owner'],
|
||
['Access', 'requester не имеет required label', 'authorization mismatch', 'проверить право на record без расширения scope в prompt'],
|
||
['Expiry', 'approval закончился до requestedAt', 'safe stop', 'запросить новое dated решение или отказаться от передачи'],
|
||
]),
|
||
h2('Воспроизводимый negative path без внешнего контекста'),
|
||
p('В коде ниже case уже содержит fixed record id и fixed authorization. Он не читает лог, не строит prompt и не делает HTTP call. Именно поэтому его можно запускать как fixture: мы проверяем, что denied class закрывается до approval override, а safe stop не выдаёт payload. В настоящей системе этот пример не заменяет data inventory или access control; он даёт небольшой контракт для теста: если доказательства нет, функция не открывает путь.'),
|
||
code([
|
||
"const request = createFixedSyntheticContextRequest('restricted-class-stop');",
|
||
'const decision = assessSyntheticAiEgress(request);',
|
||
'const stop = stopSyntheticAiEgress(decision);',
|
||
'',
|
||
'console.log(decision.accepted); // false',
|
||
'console.log(decision.classificationDecision); // denied-by-fixed-class-or-allowability',
|
||
'console.log(decision.nextAction); // owner question, no payload copy',
|
||
'console.log(stop.productionEffect); // not-attempted',
|
||
].join('\n')),
|
||
p('Fixture добавляет более жёсткие границы, чем happy path. Extra key и missing key не проходят exact contract. Forged authorization и расширенный scope не совпадают с fixed request proof. Sparse array и cyclic value не становятся «пустым списком», а закрываются без исключения. Отдельные cases проверяют requester access, authorization expiry и wrong egress scope. Это не антифрод и не DLP; это проверка, что сам учебный gate не переходит от отсутствующих фактов к неявному разрешению.'),
|
||
h2('Как оформить human review без ложного юридического вывода'),
|
||
p('Вопрос владельцу должен быть конкретнее, чем «можно ли использовать AI?». Например: «для record class X, surface Y и destination Z: какое allowability rule действует, каким immutable document подтверждается retention/training condition, кто является authority, какой requester и expiry покрыты?» Такой вопрос не утверждает, что DPA сам по себе разрешает передачу или что UI setting гарантирует место обработки. Он просит evidence, по которому организация вправе сделать собственный вывод.'),
|
||
p('Полезно разделить роли. Data owner подтверждает class и преобразование. Service or legal owner сопоставляет policy/contract с выбранной surface. Network owner подтверждает path и egress control. Security or privacy owner проверяет access and authorization process. Один человек может совмещать роли в небольшой компании, но в review всё равно стоит записать, какой вопрос он закрыл. Это снижает риск «одобрения вообще» и позволяет вернуть только спорный слой на доработку.'),
|
||
h2('Что дают источники, а чего они не дают'),
|
||
p('GitHub Docs на commit 30 апреля 2025 говорит, что в конкретной Copilot Chat surface prompt может обрабатываться вместе с context, а Bing search при включении отправляет сформированный query в Bing Search API. Это поддерживает постановку вопроса о расширенном context и отдельной внешней границе. Документ SKU isolation показывает пример endpoint-level firewall control. Он не описывает class конкретного лога, не подтверждает retention или training terms организации и не назначает человека, который может выдать approval.'),
|
||
p('NIST SP 800-207 поддерживает принцип явного, policy-based access вместо доверия к network location; NIST AI RMF помещает privacy и governance в постоянное управление риском. Оба источника не являются DPA, конфигурацией proxy или локальным classification policy. Поэтому они не используются для вывода «можно передавать». Их роль — удержать структуру решения: resource, scope, control, authority и documented risk должны быть видны раздельно.'),
|
||
h2('Короткая последовательность для реального review'),
|
||
ol([
|
||
'<strong>Остановите копирование.</strong> Сначала назовите record и class, не открывая AI surface и не отправляя сокращённый «пример».',
|
||
'<strong>Проверьте допускаемость.</strong> Сведите policy и contract evidence к выбранным plan, feature, destination и сроку, а не к бренду поставщика.',
|
||
'<strong>Сверьте путь.</strong> Отдельно подтвердите egress scope и технический control; он должен совпасть с карточкой, а не просто существовать в сети.',
|
||
'<strong>Сверьте людей и дату.</strong> Requester access, authority role, record ids и expiry должны относиться к одной операции.',
|
||
'<strong>Выпустите outcome.</strong> Либо передайте evidence в authorised workflow, либо зафиксируйте safe stop без payload и назначьте следующего владельца вопроса.',
|
||
]),
|
||
h2('Ограничения и следующий проверяемый шаг'),
|
||
p('Этот field case — полностью synthetic. Его text не содержит настоящего production log, source code, тикета, PII, secret, customer identifier, file path или network address. Скрипт не обращается к filesystem, Git, CI, network, telemetry, clock, AI model или production. A positive decision не отправляет даже synthetic string: <code>egressPerformed</code> всегда <code>false</code>, а stop object имеет <code>productionEffect</code> <code>not-attempted</code>.'),
|
||
p('Следующий шаг: проведите tabletop review на пустой карточке одного реального use case. Пусть четыре владельца независимо заполнят class, policy/contract evidence, egress scope и authorization scope, не прикладывая payload. Затем внесите один deliberate gap: например, истёкший approval или неизвестный destination. Если процесс не может назвать reason, citation и next owner без копирования данных, сначала улучшите контур review, а уже потом подключайте инструмент к рабочему контексту.'),
|
||
h2('Историческая граница апреля 2025'),
|
||
p('Все внешние утверждения ограничены immutable GitHub Docs commit от 30 апреля 2025 и финальными NIST документами 2020/2023. Нет предположений о функциях, contract terms, data routing или гарантиях, появившихся позднее. Перед фактическим use case необходимо повторить проверку обычным HTTPS GET для своих документов и зафиксировать актуальные scope, date и exceptions.'),
|
||
]);
|
||
|
||
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => Object.freeze(item));
|
||
|
||
function verifyFixture() {
|
||
const report = runAiDataPrivacyFixture();
|
||
const failed = Object.entries(report.assertions)
|
||
.filter(([, value]) => value !== true)
|
||
.map(([key]) => key);
|
||
|
||
if (failed.length > 0) {
|
||
process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
|
||
process.exitCode = 1;
|
||
return;
|
||
}
|
||
|
||
const count = Object.keys(report.assertions).length;
|
||
process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n');
|
||
}
|
||
|
||
if (process.argv.includes('--verify-fixture')) verifyFixture();
|
||
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');
|