Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions data/protocols/protocol-BATCH-2026-001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{
"protocol_id": "protocol-BATCH-2026-001",
"research_question": "How do public executive and institutional sources define governance requirements for AI-agent identities?",
"scope": "Discovery and selection of publicly accessible, canonically identifiable sources on identity, authentication, authorization, delegation, accountability, lifecycle, auditability, non-human identity, and human oversight for autonomous or agentic AI systems. Priority industries are financial services, technology and cloud, healthcare and life sciences, public sector and defense, critical infrastructure, and professional services.",
"time_period": "2020-08-15 through 2026-08-15, with pre-2020 foundational identity, zero-trust, authorization, and software-workload standards included only when they directly govern current AI-agent identity questions.",
"target_roles": [
"role-ciso",
"role-cio",
"role-cto",
"role-general-counsel",
"role-board-director",
"role-ceo"
],
"target_geographies": [
"geo-global",
"United States",
"European Union",
"United Kingdom",
"Canada",
"Singapore",
"Australia",
"India",
"Japan"
],
"target_languages": [
"lang-en"
],
"included_source_types": [
"book",
"video",
"podcast_episode",
"keynote",
"panel",
"interview",
"conference_talk",
"essay",
"newsletter",
"public_letter",
"research_report",
"academic_paper",
"working_paper",
"executive_survey",
"event_recording",
"OFF_research",
"OFF_interview",
"OFF_video",
"public_policy_document",
"other_approved"
],
"excluded_source_types": [
"Non-public or confidential material",
"Anonymous summaries and content farms",
"AI-generated summaries",
"Scraped or unofficial transcripts",
"Duplicate syndication when the original is available",
"Low-substance promotional material"
],
"inclusion_criteria": [
"The source is publicly accessible or has stable public bibliographic metadata sufficient for a metadata-only candidate.",
"The work, episode, document, speaker, author, publisher, and date can be canonically identified.",
"The source materially addresses identity, credentials, authentication, authorization, delegation, lifecycle, auditability, accountability, or oversight for AI agents or directly applicable non-human workloads.",
"The source is original, an original-publisher copy, an official author or institution copy, or a stable bibliographic record used only for identity verification.",
"The source can contribute a distinct governance, technical, legal, operational, empirical, or dissenting perspective.",
"Rights and ownership can be recorded without copying third-party text."
],
"exclusion_criteria": [
"Generic AI governance material with no meaningful identity, delegation, access-control, accountability, or agent-lifecycle content.",
"Generic human IAM, API-key, bot, or workload-identity material with no direct relevance to autonomous or agentic systems.",
"Consumer chatbot usability, model benchmark, robotics-hardware, or autonomous-vehicle identity material outside enterprise-agent governance.",
"Search snippets, inaccessible claims, ambiguous authorship, unverifiable dates, or unresolved source identity.",
"Promotional reposts, third-party uploads of uncertain rights, non-public events, not-for-attribution material, and complete third-party transcripts.",
"Sources selected only for search visibility or quota filling rather than substantive relevance."
],
"source_priority": [
"Original standard, law, policy, paper, book, recording, report, or direct statement",
"Original publisher or event host",
"Official author, institution, regulator, standards body, or company research page",
"Stable bibliographic or archival source",
"Reliable independent contextual verification",
"Secondary summary used only to locate an original"
],
"diversity_objectives": [
"Cover security, technology, legal, board, chief executive, risk, architecture, and operations perspectives without using weak sources to fill categories.",
"Include financial services, technology and cloud, healthcare and life sciences, public sector and defense, critical infrastructure, and professional services.",
"Seek global, United States, European Union, United Kingdom, Canadian, Singaporean, Australian, Indian, and Japanese relevance while recording unmet geographic goals.",
"Balance standards, policy, academic evidence, practitioner analysis, books, and primary media.",
"Balance established and emerging contributors, academic and practitioner sources, operator and investor views, and supporting and challenging positions.",
"Measure consulting, vendor, hyperscaler, and OFF-owned concentration separately."
],
"expected_limitations": [
"The controlled vocabulary currently supports English only; non-English discovery is deferred and recorded as a gap.",
"AI-agent identity terminology is emerging and overlaps non-human identity, workload identity, machine identity, delegated authorization, and zero trust.",
"Vendor and consulting sources are likely to be more visible than independent empirical research.",
"Books may be evaluated at metadata level only unless lawful full-text access is available.",
"Public sources may underrepresent failures, internal controls, and board-level decision records.",
"The 2026 endpoint may include newly published material with limited independent evaluation."
],
"planned_search_queries": [
"AI agent identity governance authentication authorization delegation auditability",
"agentic AI identity non-human identity governance standard",
"AI agents identity access management CISO CIO CTO",
"AI agent authorization OAuth workload identity SPIFFE zero trust",
"AI agent identity NIST ISO OWASP CSA W3C IETF",
"AI agent governance law policy EU UK US Singapore Canada Australia Japan India",
"AI agent identity academic paper empirical study",
"AI agent security identity conference talk podcast interview",
"agentic AI governance book autonomous agents identity",
"AI agent identity financial services healthcare public sector critical infrastructure"
],
"planned_batch_size": 120,
"created_at": "2026-08-15T10:34:21Z",
"created_by": "OpenAI Codex (machine-drafted; human review pending)",
"prompt_id": "OEII-TOPIC-DISCOVERY",
"prompt_version": "2.0",
"workflow_status": "candidate",
"machine_review_status": "machine_drafted",
"human_review_status": "pending",
"reviewed_by": null,
"reviewed_at": null,
"provenance": [
{
"source_url": "https://github.com/OpenFutureForum/executive-intelligence-index",
"accessed_at": "2026-08-15",
"retrieval_method": "Repository methodology, roadmap, schemas, and controlled vocabularies inspected before discovery",
"exact_locator": "operations/research-roadmap.yml priority 1 and OEII-TOPIC-DISCOVERY version 2.0",
"content_hash": null,
"batch_id": "BATCH-2026-001",
"prompt_id": "OEII-TOPIC-DISCOVERY",
"prompt_version": "2.0",
"notes": "The user-supplied prompt left placeholders unfilled; the repository's priority-one roadmap supplied the topic and research question."
}
],
"revision_history": []
}
9 changes: 9 additions & 0 deletions operations/batch-registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@ batches:
prompt_version: "2.0"
human_review_status: pending
publication_status: local_only
- batch_id: BATCH-2026-001
branch: research/discovery-governed-agent-identities-BATCH-2026-001
type: topic_discovery
protocol_id: protocol-BATCH-2026-001
prompt_id: OEII-TOPIC-DISCOVERY
prompt_version: "2.0"
candidate_counts: {considered: 120, accepted: 91, held: 19, rejected: 10}
human_review_status: pending
publication_status: staging_only
2 changes: 2 additions & 0 deletions operations/research-roadmap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ status: proposed_for_human_review
protocols:
- priority: 1
protocol_slug: governed-identities-for-ai-agents
discovery_batch: BATCH-2026-001
discovery_status: complete_pending_human_selection
question: How do public executive and institutional sources define governance requirements for AI-agent identities?
reason: Tests the statement/proposition model across security, architecture, legal, and board roles.
- priority: 2
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"validate:review-status": "tsx scripts/cli.ts validate review-status",
"validate:publication": "tsx scripts/cli.ts validate publication",
"validate:content": "tsx scripts/cli.ts validate content",
"validate:discovery": "tsx scripts/validate-discovery-batch.ts",
"audit:duplicates": "tsx scripts/cli.ts audit duplicates",
"audit:concentration": "tsx scripts/cli.ts audit concentration",
"audit:coverage": "tsx scripts/cli.ts audit coverage",
Expand Down
3 changes: 2 additions & 1 deletion scripts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { publicationIssues } from './lib/publication.js';
const root = process.cwd();
const args = process.argv.slice(2);
const records = loadCanonical(root);
const currentUtcDate = new Date().toISOString().slice(0, 10);

function result(name: string, details: string): void { process.stdout.write(`PASS ${name}: ${details}\n`); }
function getSchemaValidator() {
Expand Down Expand Up @@ -51,7 +52,7 @@ function validateData(): void {
else if (isbn) isbns.set(isbn, item.file);
}
for (const [key, value] of Object.entries(item.record)) {
if ((key.endsWith('_date') || key.endsWith('_at') || key === 'accessed_at') && typeof value === 'string' && value.slice(0, 10) > '2026-08-14') errors.push(`${item.file}: future-date anomaly in ${key}`);
if ((key.endsWith('_date') || key.endsWith('_at') || key === 'accessed_at') && typeof value === 'string' && value.slice(0, 10) > currentUtcDate) errors.push(`${item.file}: future-date anomaly in ${key}`);
}
if (item.record.doi && !/^10\.\d{4,9}\/[\S]+$/i.test(item.record.doi)) errors.push(`${item.file}: invalid DOI`);
if (item.schema === 'organization-reference' && item.record.canonical_cxo_ecosystem_id) {
Expand Down
128 changes: 128 additions & 0 deletions scripts/validate-discovery-batch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
import YAML from 'yaml';

const batchId = process.argv[2];
if (!batchId) throw new Error('Usage: validate-discovery-batch.ts <batch-id>');

const root = process.cwd();
const batchDir = path.join(root, 'staging', 'batches', batchId);
const readJson = (name: string) => JSON.parse(fs.readFileSync(path.join(batchDir, name), 'utf8'));
const candidates = readJson('candidate-sources.json');
const accepted = readJson('accepted-candidates.json');
const held = readJson('hold-queue.json');
const rejected = readJson('rejected-sources.json');
const duplicates = readJson('duplicate-review.json');
const candidateSchema = readJson('candidate-source.schema.json');
const searchLog = YAML.parse(fs.readFileSync(path.join(batchDir, 'search-log.yml'), 'utf8'));

const failures: string[] = [];
const warnings: string[] = [];
const passes: string[] = [];
const fail = (message: string) => failures.push(message);
const pass = (message: string) => passes.push(message);
const warn = (message: string) => warnings.push(message);

const ajv = new Ajv2020({ allErrors: true, strict: false });
addFormats(ajv);
const validateCandidate = ajv.compile(candidateSchema);
for (const candidate of candidates) {
if (!validateCandidate(candidate)) fail(`Candidate schema: ${candidate.candidate_id}: ${ajv.errorsText(validateCandidate.errors)}`);
}
if (!failures.length) pass(`Candidate schema: ${candidates.length} records conform to the batch-local schema.`);

const ids = candidates.map((item: any) => item.candidate_id);
const uniqueIds = new Set(ids);
if (uniqueIds.size !== ids.length) fail('Candidate identity: duplicate candidate_id values found.');
else pass('Candidate identity: all candidate IDs are unique.');

const expectedIds = new Set(candidates.map((item: any) => item.candidate_id));
const partitions = [...accepted, ...held, ...rejected];
if (partitions.length !== candidates.length || new Set(partitions.map((item: any) => item.candidate_id)).size !== candidates.length || partitions.some((item: any) => !expectedIds.has(item.candidate_id))) {
fail('Decision partition: accepted, held, and rejected files do not partition the candidate slate exactly.');
} else pass(`Decision partition: ${accepted.length} accepted, ${held.length} held, ${rejected.length} rejected.`);

const expectedDecision = new Map([['accept', accepted], ['hold', held], ['reject', rejected]]);
for (const [decision, items] of expectedDecision) if ((items as any[]).some((item: any) => item.decision !== decision)) fail(`Decision partition: ${decision} file contains another decision.`);

const normalizeTitle = (value: string) => value.toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/g, ' ').trim();
const normalizedTitles = new Map<string, string[]>();
for (const item of candidates) {
const key = normalizeTitle(item.title);
normalizedTitles.set(key, [...(normalizedTitles.get(key) ?? []), item.candidate_id]);
}
const titleCollisions = [...normalizedTitles.values()].filter((items) => items.length > 1);
if (titleCollisions.length) warn(`Normalized-title review: ${titleCollisions.length} exact normalized collision(s) require duplicate review.`);
else pass('Normalized-title review: no unexpected exact collisions.');

const urls = candidates.map((item: any) => item.original_url);
if (new Set(urls).size !== urls.length) fail('URL validation: duplicate original_url values found.');
else pass('URL validation: all original URLs are unique.');
for (const item of candidates) {
try { new URL(item.original_url); new URL(item.canonical_url); } catch { fail(`URL validation: invalid URL on ${item.candidate_id}.`); }
}

const unresolvedAccepted = accepted.filter((item: any) => /unresolved|unknown|speaker$/i.test(item.author_or_speaker));
if (unresolvedAccepted.length) fail(`Source identity: accepted candidates have unresolved author/speaker identity: ${unresolvedAccepted.map((item: any) => item.candidate_id).join(', ')}`);
else pass('Source identity: accepted candidates have non-empty work, author/speaker, publisher, and URL identities.');

const missingRights = candidates.filter((item: any) => !item.rights_status || !item.analysis_access_status);
if (missingRights.length) fail(`Rights validation: ${missingRights.length} candidates lack rights or access status.`);
else pass('Rights validation: every candidate has explicit access and conservative rights status.');

const missingOwnership = candidates.filter((item: any) => !item.ownership_status || !item.OFF_relationship);
if (missingOwnership.length) fail(`Ownership validation: ${missingOwnership.length} candidates lack ownership or OFF-relationship status.`);
else pass('Ownership validation: every candidate has ownership and OFF-relationship status.');

const duplicateIds = new Set(duplicates.groups.flatMap((group: any) => group.rejected_candidate_ids));
const duplicateRejects = rejected.filter((item: any) => item.likely_duplicate).map((item: any) => item.candidate_id);
if (duplicateRejects.some((id: string) => !duplicateIds.has(id)) || duplicateIds.size !== duplicateRejects.length) fail('Duplicate validation: duplicate rejects and duplicate-review groups do not match.');
else pass(`Duplicate validation: ${duplicates.groups.length} work/rendition groups resolve ${duplicateRejects.length} duplicate rejects.`);

const queryIds = new Set(searchLog.queries.map((query: any) => query.id));
const unknownQueries = candidates.flatMap((item: any) => item.search_query_ids.filter((id: string) => !queryIds.has(id)).map((id: string) => `${item.candidate_id}:${id}`));
if (unknownQueries.length) fail(`Search log: unknown query references: ${unknownQueries.join(', ')}`);
else pass(`Search log: ${queryIds.size} query families plus supplementary failures are documented.`);

const targetRoles = ['role-ciso','role-cio','role-cto','role-general-counsel','role-board-director','role-ceo'];
const acceptedRoles = new Set(accepted.flatMap((item: any) => item.relevant_roles));
const missingRoles = targetRoles.filter((role) => !acceptedRoles.has(role));
if (missingRoles.length) fail(`Coverage: no accepted candidate covers ${missingRoles.join(', ')}.`);
else pass('Coverage: every pre-registered executive role appears in the accepted set.');

const acceptedGeographies = new Set(accepted.flatMap((item: any) => item.relevant_geographies));
for (const geography of ['India','Japan']) if (!acceptedGeographies.has(geography)) warn(`Coverage gap: ${geography} has no accepted candidate.`);
if (new Set(candidates.map((item: any) => item.language)).size === 1) warn('Coverage gap: all candidates are English-language due to the active controlled vocabulary.');

const publisherCounts = new Map<string, number>();
for (const item of accepted) publisherCounts.set(item.publisher, (publisherCounts.get(item.publisher) ?? 0) + 1);
const topEntry = [...publisherCounts.entries()].sort((a, b) => b[1] - a[1])[0];
if (!topEntry) throw new Error('Concentration validation requires at least one accepted candidate.');
const [topPublisher, topCount] = topEntry;
const topShare = topCount / accepted.length;
if (topShare > 0.2) fail(`Concentration: ${topPublisher} is ${(topShare * 100).toFixed(1)}% of accepted candidates.`);
else pass(`Concentration: largest publisher label is ${topPublisher} at ${(topShare * 100).toFixed(1)}%.`);

const forbiddenRoots = ['content', 'docs', 'exports'];
const walk = (dir: string): string[] => fs.existsSync(dir) ? fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? walk(path.join(dir, entry.name)) : [path.join(dir, entry.name)]) : [];
const leaks: string[] = [];
for (const top of forbiddenRoots) {
for (const file of walk(path.join(root, top))) {
if (!/\.(json|md|html|csv|ndjson|xml|txt)$/i.test(file)) continue;
const text = fs.readFileSync(file, 'utf8');
if (text.includes(`candidate-${batchId}-`)) leaks.push(path.relative(root, file));
}
}
if (leaks.length) fail(`Staging leak: candidate IDs found outside staging in ${leaks.join(', ')}.`);
else pass('Staging leak: no candidate IDs appear in content, docs, or exports.');

if (candidates.length < 100 || candidates.length > 150) fail(`Batch size: ${candidates.length} is outside the pre-registered 100–150 range.`);
else pass(`Batch size: ${candidates.length} candidates satisfies the pre-registered range.`);

for (const message of passes) console.log(`PASS ${message}`);
for (const message of warnings) console.warn(`WARN ${message}`);
for (const message of failures) console.error(`FAIL ${message}`);
if (failures.length) process.exit(1);
Loading
Loading