Skip to content
Merged
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
27 changes: 27 additions & 0 deletions scripts/check-activity-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export async function collectFindings(root: string): Promise<Finding[]> {
for (const name of activity.variables?.reads ?? []) namespace.add(name);
}

// Declared anywhere in this workflow, on either side of any contract. `namespace` above is not
// this set: it omits declared writes, deliberately, so an included activity's read is measured.
const declaredAnywhere = new Set(namespace);
for (const activity of workflow.activities ?? []) {
for (const declaration of activity.variables?.writes ?? []) declaredAnywhere.add(declaration.name);
}

const records: ActivityRecord[] = [];
for (const activity of workflow.activities ?? []) {
const sourceWorkflowId = activitySourceWorkflow.get(activity.id) ?? workflowId;
Expand Down Expand Up @@ -140,6 +147,26 @@ export async function collectFindings(root: string): Promise<Finding[]> {
});
}
}
// A value that crosses an activity boundary with no contract at either end. Every other
// family here is measured against the declared namespace, and the namespace is assembled
// from the declarations — so a name nobody declares is invisible on BOTH sides: the
// production drops out of `writes` and the consultation drops out of `reads`, and the two
// silences look exactly like a name that is simply not used. This reads the wider `produces`
// and `mentions` to see them. A production nothing consults elsewhere is not reported: a
// utility operation's confirmation value legitimately dies with its step.
for (const name of record.derived.produces) {
if (AMBIENT_CONTEXT_IDS.has(name)) continue;
if (declaredAnywhere.has(name)) continue;
if (record.derived.persistedProductions.has(name)) continue; // destination is a file
const consumers = records
.filter((other) => other.id !== record.id && other.derived.mentions.has(name))
.map((other) => other.id);
if (consumers.length === 0) continue;
findings.push({
check: 'undeclared-crossing', site: site(record),
detail: `produces '${name}', which ${consumers.join(', ')} consults, and no contract in this workflow declares it — the value crosses an activity boundary with nothing accounting for it on either side`,
});
}
for (const name of record.declaredReads) {
if (!record.derived.reads.has(name)) {
findings.push({
Expand Down
32 changes: 30 additions & 2 deletions src/utils/activity-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,24 @@ export interface DerivedContract {
* activity's artifact contract, so the value reaches a consumer whatever else does.
*/
artifactWrites: Set<string>;
/**
* Every name a step produces, whether or not any declaration mentions it: a bound operation's
* declared output, a remap target, a checkpoint's setVariable key, a `set` action's target, a
* loop's item variable. Most are local to the activity — an output a later step of the same
* activity consumes and nothing else ever sees — so this is not a set of session writes. It is
* wider than `writes` on purpose: `writes` is narrowed to the declared namespace, and the
* namespace is assembled from the declarations, so a production no declaration mentions cannot
* appear there at all.
*/
produces: Set<string>;
/**
* Every name any step of the activity consults, before the namespace narrows it — the read-side
* counterpart to `produces`, and wider than `reads` for the same reason. A name no declaration
* mentions is absent from `reads` however plainly a technique's prose interpolates it.
*/
mentions: Set<string>;
/** Productions whose value is a file, not a bag entry: the technique declares an `#### artifact`. */
persistedProductions: Set<string>;
/**
* Every name the activity consumes, whether or not the contract requires it: the reads above,
* plus the inputs a bound operation takes when they are there and derives when they are not. An
Expand Down Expand Up @@ -313,11 +331,17 @@ export async function deriveActivityContract(args: {
const writes = new Set<string>();
const internalReads = new Set<string>();
const artifactWrites = new Set<string>();
const produces = new Set<string>();
/** Every name any step consults, before the namespace narrows it — see `produces`. */
const mentions = new Set<string>();
/** Productions whose value is a file the technique declares an `#### artifact` for. */
const persistedProductions = new Set<string>();
/** Produced so far in document order — what resolves a later read inside this activity. */
const producedSoFar = new Set<string>();

const consumes = new Set<string>();
const read = (name: string): void => {
mentions.add(name);
if (!namespace.has(name)) return;
consumes.add(name);
if (producedSoFar.has(name)) internalReads.add(name);
Expand All @@ -329,6 +353,7 @@ export async function deriveActivityContract(args: {
};
const write = (name: string): void => {
if (namespace.has(name)) writes.add(name);
produces.add(name);
producedSoFar.add(name);
};

Expand Down Expand Up @@ -371,7 +396,10 @@ export async function deriveActivityContract(args: {
const persisted = new Set(signature.artifactOutputs);
const landed = (outputId: string, bagName: string): void => {
write(bagName);
if (persisted.has(outputId) && namespace.has(bagName)) artifactWrites.add(bagName);
if (persisted.has(outputId)) {
persistedProductions.add(bagName);
if (namespace.has(bagName)) artifactWrites.add(bagName);
}
};
for (const [outputId, target] of Object.entries(binding?.outputs ?? {})) landed(outputId, target);
for (const output of signature.outputs) if (!remapped.has(output)) landed(output, output);
Expand Down Expand Up @@ -417,7 +445,7 @@ export async function deriveActivityContract(args: {
// A trigger's passContext names the values the dispatching agent relays into the child session.
for (const trigger of activity.triggers ?? []) (trigger.passContext ?? []).forEach(read);

return { reads, writes, internalReads, artifactWrites, routingReads, consumes };
return { reads, writes, internalReads, artifactWrites, produces, mentions, persistedProductions, routingReads, consumes };
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/__snapshots__/corpus-sha.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"corpusSha": "b5e034e5bc1cbeb35d578084227d6fdc17525097",
"corpusSha": "5f17da01a33bec58f36891d5af402231dd6d3a83",
"note": "Corpus commit the committed walk snapshots were generated against. Update it in the same commit that bumps the workflows submodule and re-baselines the walk (npm run baseline:stamp)."
}
2 changes: 1 addition & 1 deletion workflows
Submodule workflows updated 43 files
+3 −0 midnight-system-review/activities/01-scope-intake.yaml
+3 −0 midnight-system-review/activities/02-area-derivation.yaml
+5 −0 midnight-system-review/activities/03-evidence-probes.yaml
+5 −0 midnight-system-review/activities/04-finding-adjudication.yaml
+12 −0 midnight-system-review/activities/05-verdict-and-report.yaml
+4 −0 midnight-system-review/activities/06-publish-review.yaml
+3 −0 prism-evaluate/activities/00-scope-definition.yaml
+1 −0 prism-evaluate/activities/06-apply-mitigations.yaml
+11 −0 remediate-vuln/workflow.yaml
+15 −0 substrate-node-security-audit/activities/02-reconnaissance.yaml
+13 −0 substrate-node-security-audit/activities/03-primary-audit.yaml
+5 −0 substrate-node-security-audit/activities/04-adversarial-verification.yaml
+13 −0 substrate-node-security-audit/activities/05-report-generation.yaml
+5 −0 substrate-node-security-audit/activities/06-ensemble-pass.yaml
+10 −0 substrate-node-security-audit/activities/10-sub-crate-review.yaml
+4 −0 substrate-node-security-audit/activities/11-sub-static-analysis.yaml
+4 −0 substrate-node-security-audit/activities/12-sub-toolkit-review.yaml
+4 −0 substrate-node-security-audit/activities/14-sub-output-verification.yaml
+5 −0 substrate-node-security-audit/activities/15-sub-structured-merge.yaml
+14 −0 work-package/activities/01-start-work-package.yaml
+6 −0 work-package/activities/02-design-philosophy.yaml
+6 −0 work-package/activities/03-requirements-elicitation.yaml
+5 −0 work-package/activities/04-research.yaml
+9 −0 work-package/activities/05-implementation-analysis.yaml
+9 −0 work-package/activities/06-plan-prepare.yaml
+5 −0 work-package/activities/07-assumptions-review.yaml
+13 −0 work-package/activities/08-implement.yaml
+5 −0 work-package/activities/09-lean-coding-audit.yaml
+7 −0 work-package/activities/10-post-impl-review.yaml
+8 −0 work-package/activities/11-validate.yaml
+10 −0 work-package/activities/12-strategic-review.yaml
+18 −0 work-package/activities/13-submit-for-review.yaml
+6 −0 work-package/activities/14-complete.yaml
+2 −0 work-package/activities/15-codebase-comprehension.yaml
+3 −0 workflow-authoring/activities/06-scope-and-draft.yaml
+22 −0 workflow-authoring/activities/08-quality-review.yaml
+6 −0 workflow-authoring/activities/09-validate-and-commit.yaml
+3 −0 workflow-design/activities/01-intake-and-context.yaml
+1 −0 workflow-design/activities/03-requirements-refinement.yaml
+4 −0 workflow-design/activities/06-scope-and-draft.yaml
+10 −0 workflow-design/activities/08-quality-review.yaml
+10 −0 workflow-design/activities/09-validate-and-commit.yaml
+13 −0 workflow-design/activities/10-post-update-review.yaml
Loading