Skip to content

Commit a9cf96d

Browse files
committed
Seal workflow secrets across agent and sub-workflow handoffs
A secret resolved into an agent.run goal or a flow.subworkflow input left the scope that knew it was a secret. The engine persisted the suspend payload, the agent's task_jsonb and the child's request payload in plaintext, and Viewers read them back as the pending wait, the agent goal and the child's NormalizedPayload. The agent run's redactor never knew the value, so every echo froze into its events, native records, result and the critic's capture. The child built SecretPaths from its own rows only, so a parent wf/project secret leaked through the child's ledger, outputs and Terminal guard. The suspend wait row is now masked with the node scope's secrets for every wait kind. A task or child payload handed to another run is sealed: the public column holds the masked copy, and the exact copy plus the secret values it embeds is encrypted with the existing sensitive-payload protector (0247 adds the two nullable ciphertext columns). The executor and the child's scope build open the exact copy, so the platform never runs on a mask. The executor folds the values into its redactor inside the credential resolve, so launch, re-attach and the re-opened MCP endpoint rebuild the same fingerprint. The critic scope gets a capture redactor. A child keeps the inherited values as secrets and taints the trigger/input keys that carry them, so its records, derived scopes, map snapshots and Terminal guard treat them like its own. Tasks created inside a node scope (supervisor spawns, plan reviewers) are sealed from the ambient capture redactor, and a review child inherits its producer's secrets. The supervisor's work-plan goal is masked the same way. The run's verdicts on that work carried the value back out. The output critic is shown the exact goal, and a model restates the task it judges, so its rationale, issues and critique are masked with the task's secrets before the executor writes them into the review.completed beat, the flagged warning and the result's reviewFeedback. An acceptance argv that resolves a secret runs exactly, but the grade's evidence repeats the argv and both output tails into a team artifact a Viewer can download and onto the result. Every executor grade request now carries the task's secrets, and the grader masks the evidence, its tail, the oracle note and the detail before anything is stored. Rows written before this change carry no ciphertext and keep executing from their public columns, so in-flight waits, agent runs and child runs resume unchanged, and their redactor fingerprints still match. Those rows also keep any plaintext they already hold. A worker on the previous build does not know the sealed columns and would run a sealed child or agent from its masked public copy, so old engine and executor workers must be drained before new ones stage work.
1 parent 984c720 commit a9cf96d

42 files changed

Lines changed: 2320 additions & 59 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- 0247_sealed_agent_task_and_child_run_payload.sql
2+
--
3+
-- Two public columns held resolved workflow secrets in plaintext because they are also what the platform executes:
4+
--
5+
-- * agent_run.task_jsonb — an agent.run goal that references {{team.X}} / {{wf.X}} / {{project.p.X}} is resolved
6+
-- before staging, and the envelope was persisted whole. Viewers read it back as GetAgentRunQuery.Goal.
7+
-- * workflow_run_request.normalized_payload_json — a flow.subworkflow child's inputs, resolved in the parent, are
8+
-- the child's trigger payload. Viewers read it back as the child run's NormalizedPayload, and rerun copied it.
9+
--
10+
-- Both columns now hold a MASKED copy (every secret value the staging scope resolved replaced with [REDACTED]), and
11+
-- the exact copy plus the list of secret values it embeds live in the encrypted column added here. The executor and
12+
-- the child's scope build open that copy; every other reader keeps reading the masked column. The secret values ride
13+
-- with the exact copy so the agent run's redactor and the child run's leak guard keep treating them as secrets.
14+
--
15+
-- NULLABLE with NO DEFAULT on purpose: a catalog-only ADD COLUMN on two hot tables, and NULL is exactly "nothing was
16+
-- masked" — every row written before this migration, and every row whose payload carries no secret, keeps executing
17+
-- from its public column unchanged.
18+
19+
ALTER TABLE agent_run ADD COLUMN IF NOT EXISTS sealed_task_ciphertext TEXT NULL;
20+
21+
COMMENT ON COLUMN agent_run.sealed_task_ciphertext IS
22+
'Data Protection ciphertext of the exact AgentTask envelope and the workflow secret values it embeds. Set only when task_jsonb had a secret masked; the executor launches from it. Never returned by any read API.';
23+
24+
ALTER TABLE workflow_run_request ADD COLUMN IF NOT EXISTS sealed_payload_ciphertext TEXT NULL;
25+
26+
COMMENT ON COLUMN workflow_run_request.sealed_payload_ciphertext IS
27+
'Data Protection ciphertext of the exact normalized payload and the inherited secret values it embeds (a flow.subworkflow child). Set only when normalized_payload_json had a secret masked; the engine builds the run scope from it. Never returned by any read API.';

‎backend/src/CodeSpace.Core/Persistence/Entities/AgentRun.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ public class AgentRun : IEntity<Guid>, IAuditable
6161
/// <summary>The full <c>AgentTask</c> envelope as JSON — the run's inputs, kept whole so the envelope can evolve without a migration.</summary>
6262
public string TaskJson { get; set; } = "{}";
6363

64+
/// <summary>
65+
/// The exact task envelope, encrypted, when <see cref="TaskJson"/> had a workflow secret masked (0247). Carries the
66+
/// secret values the task embeds too, so the executor's redactor treats them as secrets. NULL — every pre-0247 row
67+
/// and every task with no secret in it — means <see cref="TaskJson"/> is itself exact. Never projected by a read API.
68+
/// </summary>
69+
public string? SealedTaskCiphertext { get; set; }
70+
6471
/// <summary>The normalized <c>AgentRunResult</c> as JSON, written on completion. NULL while in-flight.</summary>
6572
public string? ResultJson { get; set; }
6673

‎backend/src/CodeSpace.Core/Persistence/Entities/WorkflowRunRequest.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ public class WorkflowRunRequest : IEntity<Guid>
8787
/// </summary>
8888
public string NormalizedPayloadJson { get; set; } = "{}";
8989

90+
/// <summary>
91+
/// The exact payload, encrypted, when <see cref="NormalizedPayloadJson"/> had an inherited secret masked (0247 — a
92+
/// <c>flow.subworkflow</c> child whose inputs resolved a parent secret). Carries those secret values too, so the run
93+
/// keeps treating them as secrets. NULL means <see cref="NormalizedPayloadJson"/> is itself exact. Never projected.
94+
/// </summary>
95+
public string? SealedPayloadCiphertext { get; set; }
96+
9097
/// <summary>IP, user-agent, schedule fire-time, etc. — anything that's about the request itself rather than its content.</summary>
9198
public string RequestMetadataJson { get; set; } = "{}";
9299

‎backend/src/CodeSpace.Core/Persistence/EntityConfigurations/AgentRunConfiguration.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public void Configure(EntityTypeBuilder<AgentRun> builder)
1919
builder.Property(r => r.IterationKey).HasColumnName("iteration_key");
2020

2121
builder.Property(r => r.TaskJson).HasColumnName("task_jsonb").HasColumnType("jsonb");
22+
builder.Property(r => r.SealedTaskCiphertext).HasColumnName("sealed_task_ciphertext");
2223
builder.Property(r => r.ResultJson).HasColumnName("result_jsonb").HasColumnType("jsonb");
2324

2425
// P3.1a — the harness-native CLI session/thread id, promoted from result_jsonb so the rerun CONTINUE lookup

‎backend/src/CodeSpace.Core/Persistence/EntityConfigurations/WorkflowRunRequestConfiguration.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public void Configure(EntityTypeBuilder<WorkflowRunRequest> builder)
1717
builder.Property(r => r.ActivationSnapshotJson).HasColumnName("activation_snapshot_json").HasColumnType("jsonb");
1818
builder.Property(r => r.RawHeadersRedactedJson).HasColumnName("raw_headers_redacted_json").HasColumnType("jsonb");
1919
builder.Property(r => r.NormalizedPayloadJson).HasColumnName("normalized_payload_json").HasColumnType("jsonb");
20+
builder.Property(r => r.SealedPayloadCiphertext).HasColumnName("sealed_payload_ciphertext");
2021
builder.Property(r => r.RequestMetadataJson).HasColumnName("request_metadata_json").HasColumnType("jsonb");
2122
builder.Property(r => r.VerificationResultJson).HasColumnName("verification_result_json").HasColumnType("jsonb");
2223
}

0 commit comments

Comments
 (0)