Skip to content
Open
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
4 changes: 4 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ _Avoid_: autonomous plan, agent hunch, task queue
A versioned decision contract that controls scheduling, approval consumption, budget stops, and recovery deduplication for a research campaign without creating target authorization or approval authority.
_Avoid_: YOLO mode, blanket approval, autonomous permission

**Research Campaign Definition**:
A frozen, human-selected plan connecting one ranked ecosystem opportunity, admitted Target Recipe, authorization scope, autonomy policy, thread topology, harness admissions, budgets, exit criteria, honesty mode, recovery policy, and stop conditions before launch.
_Avoid_: agent plan, benchmark manifest, target authorization
**Shared Terminal Session**:
A project/thread-scoped interactive shell session whose input, output, resize events, interrupts, approvals, and actor attribution are visible to both the researcher and approved agent automation.
_Avoid_: generic shell bridge, hidden agent shell, human terminal takeover
Expand Down Expand Up @@ -161,6 +164,7 @@ _Avoid_: hidden gold, judge assertion
- A **Research Observation** may preserve several external identifiers and versioned score assessments; each remains attributable to the Observation's citations and time.
- A **Research Observation** may cite a message from another project thread when that discussion materially supports or contextualizes it.
- A **Campaign Autonomy Policy** may consume an already matching durable approval, but it never mints one, widens target scope, extends an expired decision, overrides a denial, or blindly repeats an unknown side effect.
- A **Research Campaign Definition** may become ready for the execution gate only after human selection and admission checks; it never launches a target, creates execution authority, or imports a global vulnerability claim.
- An **Investigation Entity** references a canonical project record when one exists instead of copying that record into the **Investigation Graph**.
- An **Investigation Assertion** may be supported, contradicted, derived, revised, rejected, or left unresolved without changing the canonical record it discusses.
- An **Investigation Citation** identifies why an **Investigation Assertion** exists; an **Artifact** remains the durable evidence object.
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ The project Investigation Graph is an assertion layer over existing records, not

Campaign autonomy is a scheduling and recovery policy, not an authorization source. Manual, bounded, and fully automated modes share the same durable target ledger and exact-intent enforcement. Only active probes, downloads, and shell commands may consume an explicitly declared exact preauthorization in a non-manual mode; credential tests, browser mutations, writes, exploit validation, and patching require a fresh exact decision. Denied, expired, mismatched, or consumed approvals stop the transition. Recovery reuses successful side effects and pauses to reconcile unknown outcomes before any replay. Every mode obeys conjunctive active-time, wall-time, cost, and action ceilings and preserves the policy/mode, normalized intent, authorization and approval references, Tool Run/effect fingerprint, budget transition, and raw redacted evidence.

A Research Campaign Definition freezes the selection boundary without launching work. It joins one human-selected ranked opportunity to a current-supported Target Recipe for organic discovery (or a separately labeled historical control), target authorization scope, campaign-autonomy policy, project-memory-scoped planning/research/validation/reporting threads, admitted harness manifests, conjunctive budgets, exit criteria, honesty boundaries, recovery deduplication, and stop conditions. Admission requires lifecycle/reset/teardown evidence and complete coverage, rejected-lead, impact-validation, patch, disclosure, reusable-method, accounting, terminal, and cleanup dispositions. The admitted definition exposes immutable references for the later execution gate but never creates an approval, target mutation, or vulnerability assertion.
A Research Priority is an unresolved, citation-backed current assertion ranked for follow-up. Its deterministic score weights objective relevance (25%), evidence gap (20%), expected information gain (20%), target importance (15%), inverse predicate cost (8%), inverse predicate risk (7%), and authorization readiness (5%). Authorization readiness comes from the durable target ledger. Deliberately turning a Research Priority into a Task uses the existing Task workflow and a unique assertion-task receipt; it never schedules work, creates an approval, runs a tool, or promotes a Finding.

The former generic security-graph repository is retired. Historical database tables may remain so existing local data is not destructively dropped, but no product path writes them and they are not authoritative. The only graph ownership boundaries are the global Security Knowledge Graph and each project's Investigation Graph.
Expand Down
342 changes: 342 additions & 0 deletions src/server/research/campaign-admission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
import { createHash } from "node:crypto";

import type { CampaignAutonomyMode } from "./campaign-autonomy-policy";

export const RESEARCH_CAMPAIGN_DEFINITION_VERSION =
"research-campaign-definition-v1";

export type ResearchCampaignDefinition = {
version: typeof RESEARCH_CAMPAIGN_DEFINITION_VERSION;
campaignId: string;
projectId: string;
studyLane: "organic-discovery" | "historical-control";
selection: {
decisionTicketId: string;
selectedBy: string;
selectedAt: string;
opportunitySnapshotDigest: string;
opportunityCandidateId: string;
opportunityStatus: "ranked" | "held" | "rejected";
sourceSignalIds: string[];
vulnerabilityClaimImported: boolean;
};
targetRecipe: {
recipeId: string;
recipeDigest: string;
revisionLane: "current-supported" | "historical-control";
targetLocator: string;
configurationId: string;
lifecycleEvidenceArtifactIds: string[];
resetVerified: boolean;
teardownVerified: boolean;
};
authorization: {
targetAuthorizationIds: string[];
scopeDigest: string;
networkProfile: "offline" | "approved-targets";
approvalPolicyVersion: string;
};
autonomy: {
policyVersion: "campaign-autonomy-policy-v1";
mode: CampaignAutonomyMode;
};
threads: Array<{
threadId: string;
role: "planning" | "research" | "validation" | "reporting";
projectMemoryScopeId: string;
artifactHandoffRequired: boolean;
}>;
harnesses: Array<{
harnessId: string;
admissionRecordId: string;
manifestDigest: string;
admitted: boolean;
stage: "sentinel" | "campaign" | "reporting-only";
}>;
budgets: {
activeWorkingMs: number;
wallMs: number;
candidateCostUsd: number;
targetActionLimit: number;
contextTokens: number;
maxOutputTokens: number;
};
exitCriteria: {
coverageArtifactRequired: boolean;
rejectedLeadRecordsRequired: boolean;
impactValidationProtocol: "impact-validation-v1";
validatedFindingEvidenceRequired: boolean;
patchEvidenceForValidatedFindings: boolean;
disclosureReadinessDispositionRequired: boolean;
reusableMethodCandidateDispositionRequired: boolean;
costAndTimeLedgerRequired: boolean;
targetResetAndTeardownRequired: boolean;
terminalStatusRequired: boolean;
stopWhenEvidenceSufficient: boolean;
};
honesty: {
knownAnswersCandidateVisible: false;
hiddenOracleCandidateVisible: false;
priorTargetFindingsCandidateVisible: false;
historicalControlsSeparated: boolean;
runModeLabel: "organic-hunt" | "historical-control";
};
recovery: {
sideEffectFingerprintsRequired: boolean;
unknownEffectsRequireReconciliation: boolean;
continuationArtifactRequiredOnPause: boolean;
};
stopConditions: string[];
};

export type ResearchCampaignAdmission = {
version: typeof RESEARCH_CAMPAIGN_DEFINITION_VERSION;
campaignId: string;
status: "ready-for-execution-gate" | "blocked";
failures: string[];
executionAuthorityCreated: false;
launchPerformed: false;
definitionDigest?: `sha256:${string}`;
frozenReferences?: {
opportunitySnapshotDigest: string;
targetRecipeDigest: string;
scopeDigest: string;
harnessManifestDigests: string[];
};
};

const REQUIRED_THREAD_ROLES: ResearchCampaignDefinition["threads"][number]["role"][] =
["planning", "research", "validation", "reporting"];

export function admitResearchCampaign(
definition: ResearchCampaignDefinition,
): ResearchCampaignAdmission {
const failures = [
...validateIdentity(definition),
...validateSelection(definition),
...validateRecipe(definition),
...validateAuthorization(definition),
...validateThreads(definition),
...validateHarnesses(definition),
...validateBudgets(definition),
...validateExitCriteria(definition),
...validateHonestyAndRecovery(definition),
];
const definitionDigest =
`sha256:${createHash("sha256").update(stableJson(definition)).digest("hex")}` as const;
return {
version: RESEARCH_CAMPAIGN_DEFINITION_VERSION,
campaignId: definition.campaignId,
status: failures.length === 0 ? "ready-for-execution-gate" : "blocked",
failures,
executionAuthorityCreated: false,
launchPerformed: false,
...(failures.length === 0
? {
definitionDigest,
frozenReferences: {
opportunitySnapshotDigest:
definition.selection.opportunitySnapshotDigest,
targetRecipeDigest: definition.targetRecipe.recipeDigest,
scopeDigest: definition.authorization.scopeDigest,
harnessManifestDigests: definition.harnesses.map(
(item) => item.manifestDigest,
),
},
}
: {}),
};
}

function validateIdentity(definition: ResearchCampaignDefinition) {
const failures: string[] = [];
if (definition.version !== RESEARCH_CAMPAIGN_DEFINITION_VERSION) {
failures.push("campaign_definition_version_unsupported");
}
for (const [name, value] of Object.entries({
campaignId: definition.campaignId,
projectId: definition.projectId,
decisionTicketId: definition.selection.decisionTicketId,
selectedBy: definition.selection.selectedBy,
})) {
if (!value.trim()) failures.push(`${name}_missing`);
}
if (!Number.isFinite(Date.parse(definition.selection.selectedAt))) {
failures.push("selection_timestamp_invalid");
}
if (
definition.stopConditions.length === 0 ||
definition.stopConditions.some((item) => !item.trim())
) {
failures.push("stop_conditions_missing");
}
return failures;
}

function validateSelection(definition: ResearchCampaignDefinition) {
const failures: string[] = [];
if (definition.selection.opportunityStatus !== "ranked") {
failures.push("opportunity_not_ranked");
}
if (!digest(definition.selection.opportunitySnapshotDigest)) {
failures.push("opportunity_snapshot_digest_invalid");
}
if (!definition.selection.opportunityCandidateId.trim())
failures.push("opportunity_candidate_missing");
if (definition.selection.sourceSignalIds.length === 0)
failures.push("source_signal_references_missing");
if (definition.selection.vulnerabilityClaimImported) {
failures.push("global_signal_cannot_import_vulnerability_claim");
}
return failures;
}

function validateRecipe(definition: ResearchCampaignDefinition) {
const failures: string[] = [];
for (const [name, value] of Object.entries({
recipeId: definition.targetRecipe.recipeId,
targetLocator: definition.targetRecipe.targetLocator,
configurationId: definition.targetRecipe.configurationId,
})) {
if (!value.trim()) failures.push(`${name}_missing`);
}
if (!digest(definition.targetRecipe.recipeDigest))
failures.push("target_recipe_digest_invalid");
if (definition.targetRecipe.lifecycleEvidenceArtifactIds.length === 0) {
failures.push("target_lifecycle_evidence_missing");
}
if (!definition.targetRecipe.resetVerified)
failures.push("target_reset_not_verified");
if (!definition.targetRecipe.teardownVerified)
failures.push("target_teardown_not_verified");
if (
definition.studyLane === "organic-discovery" &&
definition.targetRecipe.revisionLane !== "current-supported"
) {
failures.push("organic_discovery_requires_current_supported_revision");
}
if (
definition.studyLane === "historical-control" &&
definition.targetRecipe.revisionLane !== "historical-control"
) {
failures.push("historical_study_requires_control_revision");
}
return failures;
}

function validateAuthorization(definition: ResearchCampaignDefinition) {
const failures: string[] = [];
if (definition.authorization.targetAuthorizationIds.length === 0) {
failures.push("target_authorization_missing");
}
if (!digest(definition.authorization.scopeDigest))
failures.push("authorization_scope_digest_invalid");
if (!definition.authorization.approvalPolicyVersion.trim()) {
failures.push("approval_policy_version_missing");
}
if (definition.autonomy.policyVersion !== "campaign-autonomy-policy-v1") {
failures.push("campaign_autonomy_policy_mismatch");
}
return failures;
}

function validateThreads(definition: ResearchCampaignDefinition) {
const failures: string[] = [];
const ids = new Set<string>();
for (const thread of definition.threads) {
if (!thread.threadId.trim() || ids.has(thread.threadId))
failures.push("thread_identity_invalid");
ids.add(thread.threadId);
if (thread.projectMemoryScopeId !== definition.projectId) {
failures.push(`thread_memory_scope_mismatch:${thread.role}`);
}
if (!thread.artifactHandoffRequired)
failures.push(`artifact_handoff_not_required:${thread.role}`);
}
for (const role of REQUIRED_THREAD_ROLES) {
if (!definition.threads.some((thread) => thread.role === role)) {
failures.push(`thread_role_missing:${role}`);
}
}
return failures;
}

function validateHarnesses(definition: ResearchCampaignDefinition) {
if (definition.harnesses.length === 0) return ["admitted_harness_missing"];
const failures: string[] = [];
for (const harness of definition.harnesses) {
if (!harness.harnessId.trim() || !harness.admissionRecordId.trim()) {
failures.push("harness_identity_missing");
}
if (!digest(harness.manifestDigest))
failures.push("harness_manifest_digest_invalid");
if (!harness.admitted)
failures.push(`harness_not_admitted:${harness.harnessId}`);
}
return failures;
}

function validateBudgets(definition: ResearchCampaignDefinition) {
const failures: string[] = [];
for (const [name, value] of Object.entries(definition.budgets)) {
if (!Number.isFinite(value) || value <= 0)
failures.push(`campaign_budget_invalid:${name}`);
}
return failures;
}

function validateExitCriteria(definition: ResearchCampaignDefinition) {
const { impactValidationProtocol, ...booleanCriteria } =
definition.exitCriteria;
const missing = Object.entries(booleanCriteria)
.filter(([, value]) => value !== true)
.map(([name]) => `exit_criterion_missing:${name}`);
if (impactValidationProtocol !== "impact-validation-v1") {
missing.push("impact_validation_protocol_mismatch");
}
return missing;
}

function validateHonestyAndRecovery(definition: ResearchCampaignDefinition) {
const failures: string[] = [];
if (
definition.studyLane === "organic-discovery" &&
definition.honesty.runModeLabel !== "organic-hunt"
) {
failures.push("organic_run_mode_label_mismatch");
}
if (
definition.studyLane === "historical-control" &&
definition.honesty.runModeLabel !== "historical-control"
) {
failures.push("historical_run_mode_label_mismatch");
}
if (!definition.honesty.historicalControlsSeparated) {
failures.push("historical_controls_not_separated");
}
if (!definition.recovery.sideEffectFingerprintsRequired) {
failures.push("side_effect_fingerprints_not_required");
}
if (!definition.recovery.unknownEffectsRequireReconciliation) {
failures.push("unknown_effect_reconciliation_not_required");
}
if (!definition.recovery.continuationArtifactRequiredOnPause) {
failures.push("continuation_artifact_not_required");
}
return failures;
}

function digest(value: string) {
return /^sha256:[0-9a-f]{64}$/.test(value);
}

function stableJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "null";
}
1 change: 1 addition & 0 deletions src/server/research/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./campaign-autonomy-policy";
export * from "./campaign-admission";
export * from "./resource-aware-execution";
export {
type FinishResearchInput,
Expand Down
Loading