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 @@ -56,6 +56,10 @@ _Avoid_: reasoning text, inferred edge, model explanation
An unresolved, citation-backed Investigation Assertion ranked for follow-up after accounting for objective relevance, missing evidence, expected information gain, target importance, cost, risk, and authorization readiness.
_Avoid_: autonomous plan, agent hunch, task queue

**Target Recipe**:
A versioned, portable contract for reproducing one authorized research target configuration, including immutable upstream identity, fixtures, lifecycle, isolation, evidence, provenance, and required authorization intent.
_Avoid_: benchmark task, Compose file, target manifest, deployment script

**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
6 changes: 6 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ Workspace tools are conservative by default:
- generic workspace command execution is disabled;
- approved commands go through app-owned lab or SSH command tools where target mode, approvals, and artifacts can be enforced.

## Target Recipes And Research Campaigns

A Target Recipe is the product-owned, portable contract for one reproducible research target configuration. It pins source and image revisions, fixture identity, script-backed lifecycle steps, loopback or internal-only exposure, resource and isolation limits, reset and teardown verification, evidence paths, provenance, and the authorization intent a later campaign must satisfy. Recipe admission produces a stable digest and target locator, but never creates target authorization or approval. Benchmark discovery remains a separate registry so hidden scoring material and replay controls cannot enter ordinary project memory.

Harness adapters consume admitted recipes rather than embedding Compose or target-specific lifecycle knowledge. Current supported revisions are the genuine-discovery lane; historical vulnerable revisions remain explicitly labeled controls. The first tracer recipe and campaign persistence are added only after their decision tickets settle the remaining provisioning and autonomy details.

## Evidence Path

Generated evidence and uploads should flow through `src/server/evidence/artifact-service.ts`.
Expand Down
1 change: 1 addition & 0 deletions src/server/targets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
TargetScopeError,
type TargetScopeInput,
} from "./require-target";
export * from "./target-recipe";

export type AuthorizationRecord = {
id: string;
Expand Down
305 changes: 305 additions & 0 deletions src/server/targets/target-recipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import { createHash } from "node:crypto";

import { z } from "zod";

export const TARGET_RECIPE_SCHEMA_VERSION = 1 as const;

export const TARGET_RECIPE_ACTION_CLASSES = [
"passive-review",
"active-probe",
"credential-test",
"browser-mutation",
"download",
"write",
"shell-command",
"exploit-validation",
"patch",
] as const;

const nonEmptyString = z.string().trim().min(1);
const sha256Digest = z.string().regex(/^sha256:[0-9a-f]{64}$/);
const gitCommit = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/);
const relativeScript = nonEmptyString.refine(
(value) =>
!value.startsWith("/") &&
!value.startsWith("\\") &&
!value.startsWith("~") &&
!/^[a-z]:[\\/]/i.test(value) &&
!value.split(/[\\/]/).some((segment) => segment === ".."),
"must be a relative path that stays inside the recipe directory",
);

const lifecycleStepSchema = z
.object({
script: relativeScript,
expectedMaxDurationMs: z.number().int().positive(),
})
.strict();

const targetRecipeSchema = z
.object({
version: z.literal(TARGET_RECIPE_SCHEMA_VERSION),
identity: z
.object({
id: nonEmptyString.regex(/^[a-z0-9][a-z0-9._/-]*$/),
displayName: nonEmptyString,
family: nonEmptyString,
variant: nonEmptyString,
upstreamUrl: z.string().url(),
licenseDecision: nonEmptyString,
})
.strict(),
revision: z
.object({
lane: z.enum(["current-supported", "historical-control"]),
sourceRef: nonEmptyString,
sourceCommit: gitCommit,
releaseDate: nonEmptyString,
releaseArtifactDigest: sha256Digest.optional(),
imageDigests: z.array(sha256Digest).min(1),
dependencyLockDigest: sha256Digest.optional(),
})
.strict(),
configuration: z
.object({
profileId: nonEmptyString,
fixtureVersion: nonEmptyString,
components: z
.array(
z
.object({
name: nonEmptyString,
version: nonEmptyString,
role: z.enum([
"application",
"database",
"cache",
"proxy",
"local-fake",
]),
})
.strict(),
)
.min(1),
syntheticIdentities: z
.array(
z.object({ id: nonEmptyString, role: nonEmptyString }).strict(),
)
.min(1),
externalServiceFakes: z
.array(
z
.object({
service: nonEmptyString,
implementation: nonEmptyString,
})
.strict(),
)
.optional(),
})
.strict(),
lifecycle: z
.object({
acquire: lifecycleStepSchema.optional(),
build: lifecycleStepSchema.optional(),
initialize: lifecycleStepSchema,
start: lifecycleStepSchema,
readiness: lifecycleStepSchema,
reset: lifecycleStepSchema,
verifyReset: lifecycleStepSchema,
stop: lifecycleStepSchema,
destroy: lifecycleStepSchema,
verifyDestroy: lifecycleStepSchema,
})
.strict(),
runtime: z
.object({
exposure: z.enum(["internal-only", "loopback"]),
hostBind: z.literal("127.0.0.1").optional(),
setupNetworkProfile: z.enum(["offline", "package-egress"]),
researchNetworkProfile: z.enum(["offline", "approved-targets"]),
isolationClass: z.enum(["container", "microvm"]),
dockerSocket: z.literal("none"),
privileged: z.literal(false),
resourceClass: z.enum(["micro", "small", "medium"]),
limits: z
.object({
cpus: z.number().positive(),
memoryMb: z.number().int().positive(),
pids: z.number().int().positive(),
writableMb: z.number().int().positive(),
})
.strict(),
})
.strict()
.superRefine((runtime, context) => {
if (
runtime.exposure === "loopback" &&
runtime.hostBind !== "127.0.0.1"
) {
context.addIssue({
code: "custom",
path: ["hostBind"],
message: "loopback exposure requires hostBind 127.0.0.1",
});
}
if (
runtime.exposure === "internal-only" &&
runtime.hostBind !== undefined
) {
context.addIssue({
code: "custom",
path: ["hostBind"],
message: "internal-only exposure must not publish a host binding",
});
}
}),
parallelism: z
.object({
instanceNameTemplate: nonEmptyString.refine(
(value) => value.includes("{instanceId}"),
"must include {instanceId}",
),
composeProjectNameTemplate: nonEmptyString.refine(
(value) => value.includes("{instanceId}"),
"must include {instanceId}",
),
dynamicPortPolicy: z.enum(["none", "loopback-allocated"]),
capacityWeight: z.number().int().positive(),
})
.strict(),
provenance: z
.object({
recipeRevision: nonEmptyString,
createdAt: z.string().datetime(),
reviewedAt: z.string().datetime(),
buildArtifactIds: z.array(nonEmptyString),
intelligenceSnapshotId: nonEmptyString.optional(),
})
.strict(),
evidence: z
.object({
paths: z
.array(
z
.object({
kind: z.enum([
"log",
"http-capture",
"database-snapshot",
"filesystem-diff",
"trace",
]),
path: relativeScript,
redactionPolicy: nonEmptyString,
})
.strict(),
)
.min(1),
})
.strict(),
safety: z
.object({
authorizationMode: z.literal("durable-ledger-required"),
permittedActionClasses: z
.array(z.enum(TARGET_RECIPE_ACTION_CLASSES))
.min(1),
prohibitedActionClasses: z.array(z.enum(TARGET_RECIPE_ACTION_CLASSES)),
stopConditions: z.array(nonEmptyString).min(1),
syntheticSecretPolicy: z.literal("per-run-canaries"),
disclosureContact: nonEmptyString.optional(),
})
.strict()
.superRefine((safety, context) => {
const prohibited = new Set(safety.prohibitedActionClasses);
for (const actionClass of safety.permittedActionClasses) {
if (prohibited.has(actionClass)) {
context.addIssue({
code: "custom",
path: ["permittedActionClasses"],
message: `${actionClass} cannot be both permitted and prohibited`,
});
}
}
if (!safety.permittedActionClasses.includes("passive-review")) {
context.addIssue({
code: "custom",
path: ["permittedActionClasses"],
message: "every research recipe must permit passive-review",
});
}
}),
})
.strict()
.superRefine((recipe, context) => {
if (
recipe.runtime.exposure === "internal-only" &&
recipe.parallelism.dynamicPortPolicy !== "none"
) {
context.addIssue({
code: "custom",
path: ["parallelism", "dynamicPortPolicy"],
message: "internal-only recipes cannot allocate a published host port",
});
}
});

export type TargetRecipe = z.infer<typeof targetRecipeSchema>;

export type AdmittedTargetRecipe = {
recipe: TargetRecipe;
recipeDigest: `sha256:${string}`;
targetLocator: `recipe:${string}@${string}`;
requiredAuthorization: {
mode: "durable-ledger-required";
actionClasses: TargetRecipe["safety"]["permittedActionClasses"];
networkProfile: TargetRecipe["runtime"]["researchNetworkProfile"];
};
};

export function parseTargetRecipe(
value: unknown,
source = "target recipe",
): TargetRecipe {
const result = targetRecipeSchema.safeParse(value);
if (!result.success) {
const details = result.error.issues
.map((issue) => `${issue.path.join(".") || "recipe"}: ${issue.message}`)
.join("; ");
throw new Error(
`${source} is not an admissible Target Recipe v1: ${details}`,
);
}
return result.data;
}

export function admitTargetRecipe(
value: unknown,
source?: string,
): AdmittedTargetRecipe {
const recipe = parseTargetRecipe(value, source);
const canonical = stableJson(recipe);
const recipeDigest =
`sha256:${createHash("sha256").update(canonical).digest("hex")}` as const;
return {
recipe,
recipeDigest,
targetLocator: `recipe:${recipe.identity.id}@${recipe.provenance.recipeRevision}`,
requiredAuthorization: {
mode: recipe.safety.authorizationMode,
actionClasses: recipe.safety.permittedActionClasses,
networkProfile: recipe.runtime.researchNetworkProfile,
},
};
}

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";
}
Loading
Loading