Files
progcode/web/scripts/upgrade-2025-04.mjs
T
huncode 7c5b19c960
Build and deploy / deploy (push) Successful in 18s
edit full article archive to publication standard
2026-07-31 23:08:19 +03:00

865 lines
81 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
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('&nbsp;', ' ')
.replaceAll('&quot;', '"')
.replaceAll('&#039;', "'")
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&amp;', '&')
.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?»'),
p('В том же вопросе нужно назвать 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');