Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request

use Osysharp.Privacy; use Osysharp.Agents; · MarkAgentWorkAbout(task, subject) · MarkAssignmentAbout(a, subject) · MarkItemAbout(item, subject) · categories agents.trace, agents.record, llm.calls, assignments.about, assignments.people, repositories.<kind> · VerifyTrace(task).Erased

An app that uses `Osysharp.Privacy` beside the agent kits gets a holder for each without writing one: `Osysharp.Privacy.Agents` (what an agent was shown and answered, and the record that it worked), `Osysharp.Privacy.Llm` (the model meter), `Osysharp.Privacy.Assignments` (the people on an assignment and the person it is about) and `Osysharp.Privacy.WorkRepositories` (the items a piece of work keeps, each kind for its own time). What is done FOR or BY a person is found by their account; what is ABOUT a person by what the app marked it with. A copy carries every word; an erasure removes the words and leaves the record — an agent's hash chain still verifies and still finds a changed line, an assignment reads as work done for "a former customer", an item keeps its place — and each kind of thing is cleared for everybody when its own period ends.

preview2 examples compiled by CIagentskitprivacygdpr

Summary#

app Library {
  use Osysharp.Privacy;
  use Osysharp.Agents;      // …and Osysharp.Privacy.Agents and Osysharp.Privacy.Llm arrive by themselves
}

var outcome = await Agents.Of<Desk>().Do(new DraftReminder(), loan);
MarkAgentWorkAbout(outcome.Task, new DataSubject { Emails = [loan.BorrowerEmail] });   // whose data that work is

Signature#

// Osysharp.Privacy.Agents — completes Osysharp.Privacy + Osysharp.Agents
void MarkAgentWorkAbout(Guid task, DataSubject subject)        // work ABOUT somebody: by account, address or number
List<AgentTask> AgentWorkOf(DataSubject subject)               // the top piece of each piece of work for or about them
class AgentsPersonalData : IPersonalDataHolder                 // discovered — categories agents.trace, agents.record
app.AgentsPrivacy = new AgentsPrivacySetup { KeepContentMonths = 1, KeepRecordMonths = 7 };

// Osysharp.Privacy.Llm — completes Osysharp.Privacy + Osysharp.Llm
class ModelCallsPersonalData : IPersonalDataHolder             // discovered — category llm.calls
app.ModelCallsPrivacy = new ModelCallsPrivacySetup { KeepMonths = 13 };

// Osysharp.Llm — what the meter's holder calls
int  ForgetModelCallRequester(Guid requester, DateTime? before = null)   // needs ManagesModelSpend
bool ModelCallsInFlightFor(Guid requester)

// Osysharp.Privacy.Assignments — completes Osysharp.Privacy + Osysharp.Assignments
void MarkAssignmentAbout(Assignment a, DataSubject subject)    // and everything opened under it, and what its work stands for
List<Assignment> AssignmentsAbout(DataSubject subject)
class AssignmentsPersonalData : IPersonalDataHolder            // discovered — categories assignments.about, assignments.people
class AssignmentPrivacyRules { virtual List<string> PersonalWords(Assignment a) }   // the words that name them
app.AssignmentsPrivacy = new AssignmentsPrivacySetup { KeepAboutMonths = 24, KeepPeopleMonths = 0, StandIn = "a former customer",
                                                       FormerColleague = "a former colleague", AlwaysBlank = false, Rules = … };

// Osysharp.Privacy.WorkRepositories — completes Osysharp.Privacy + Osysharp.WorkRepositories
void MarkItemAbout(RepositoryItem item, DataSubject subject)
List<RepositoryItem> ItemsAbout(DataSubject subject, DataCategory category)
class WorkRepositoriesPersonalData : IPersonalDataHolder       // discovered — repositories.<kind>, .items, .contributions
app.WorkRepositoriesPrivacy = new WorkRepositoriesPrivacySetup { KeepOtherMonths = 0 };

Description#

What do I have to write?#

Nothing for what is done FOR or BY a signed-in person, and one line for what is ABOUT somebody. Every holder is discovered, and each package completes the two kits it sits between, so osy lock includes it in any app that uses Osysharp.Privacy with Osysharp.Agents, Osysharp.Llm, Osysharp.Assignments or Osysharp.WorkRepositories.

Say it once, where the work starts#

A mark on an assignment covers what hangs under it: the assignments opened under it, the row each piece of attached work stands for — an agent's receipt — and every item in its repository; and the agent work a repository item was written in. So a case opened about a customer needs one line — MarkAssignmentAbout(a, new DataSubject { Emails = [order.CustomerEmail] }) — and one request then reaches the case, the agent's record of the work it did on it, and what that work found, each through its own holder. Each holder says which rows hang under which (ISubjectLinks, The Privacy kit — erasure that keeps only what the company must or may); nothing is wired between them.

Whose data is a piece of agent work?#

  • Done for a person — they asked, or somebody asked on their behalf (For(person)). The receipt names them (AgentTask.Requester, StartedBy), and a request finds it by the account it proved.
  • About a person — a reminder drafted about a borrower, a claim checked for a customer. Only the app knows, so it says: MarkAgentWorkAbout(outcome.Task, subject), with an account, an address or a number. The note keeps a keyed fingerprint of an address, never the address; anybody doing the work may write one and only the privacy desk reads it back. It is Osysharp.Privacy's MarkAbout (The Privacy kit — erasure that keeps only what the company must or may) with the kind filled in.

A piece of work is found whole: the top piece and everything it handed on share one record and one chain. Work nobody marked is still found as the work of whoever gave it, but not by the address of the person it was about.

What is held, and for how long?#

categorywhatkept by default
agents.tracewhat the work said: every line's content, what was asked and answered when it asked for help, the result1 month after it ended
agents.recordthat it happened: agent, skill, who it was for, what it was about, each step's sentence, model, tool and cost, how it ended, the chain7 months after it ended
llm.callsthe model calls made for a person: when, which model, tokens, cost — the meter keeps no text13 months on their name
agents.correctionswhat somebody wrote to correct work done for or about a person, and the instructions an agent is given for that person's work (InstructionDraft.For, or marked with MarkAgentInstructionAbout)7 months after it was written, and never longer than the record of the work it corrects
agents.teachingthat a person taught: the corrections they wrote and the instructions they proposed, activated, retired or rolled back — by their accountwhile the agent is told it
agents.oversightthat a person oversaw the agents: the [[agent-autonomy#standingstanding rules]] they gave or took back, the times they paused, resumed or sponsored an agent or were named its sponsor, the questions an agent put to them and their answers, the proposals sent to them or decided by them — by their accountwhile what was decided is on record
agents.handoversthat a person handed an inbox item to an agent — with Osysharp.Agents.HumanRequests, through Osysharp.Privacy.Agents.HumanRequestswhile the item is kept

An erasure of agents.corrections takes each correction's words and erases an instruction for the person — retired first, which is the agent's next instruction set, made by nobody; that the work was corrected stays as a count. A restriction withholds such an instruction, so the agent stops being sent it. A standing rule for the person's work ("from now on, alone") is in agents.corrections too: erased, it is taken back and no longer says whose work it was (AgentStandingRule.ErasedAt) — so it never comes to cover everybody's; kept out of use, nobody reads it, and the agent proposes again. An erasure of agents.teaching takes the person off what they taught; what was said and decided stays, and the agent goes on being told it. An erasure of agents.oversight or agents.handovers takes the person off in the same way (ForgetWhoOversawAgents, ForgetWhoHandedWorkToAgents); it waits while a question or a proposal still waits on them, while they sponsor an agent, or while an agent still holds an item they handed over.

All of them are held on legitimate interest, so an erasure erases them unless somebody records compelling grounds. The defaults are the app's (AgentsPrivacySetup, ModelCallsPrivacySetup) and the tenant's on the retention page. Seven months is six and a margin: where the EU AI Act applies, Art. 19 and Art. 26(6) ask for a high-risk system's logs to be kept at least six months. Whether it applies is the tenant's to establish.

What about assignments?#

Two different people are in one. The people ON it — who asked, who owns it, who did the work — are found by their account (assignments.people); an erasure takes them off: their seat goes, nothing on it is theirs, and what they did reads as done by Somebody no longer here. The person it is ABOUT is found by the mark (assignments.about); an erasure replaces the words that say who it was about by the app's stand-in — "a former customer" — in every sentence, and leaves what was done and how it ended. A request proves an address, not a name, so the app's AssignmentPrivacyRules.PersonalWords(a) names the words; when it names none and no account is known, every sentence is erased whole rather than a name left standing. An assignment about somebody is erased once it is closed — until then the request waits — and loses those words for everybody 24 months after it closed.

What about a repository's items?#

Each kind the app keeps for a time of its own (KindRule.RetainFor, Work repositories — what an assignment knows, kept as items (WorkRepositories kit)) is a category of its own — repositories.document, repositories.finding — with that period, which the tenant reads and changes on the retention page; every other kind is repositories.items. An item about a person is erased when they ask and when its kind's period ends: what it said goes from every version, with its notes and its title on the assignment's activity; that there was one, what kind, who added it and when, stay. Who reads which item is the same before and after. A contributor who leaves (repositories.contributions, by account) is taken off what they added, which stays.

What does an erasure leave of an agent's work?#

What was said is emptied, for the auditor too. The person is taken off the work: it is nobody's, about nothing, and each line's sentence reads Erased. What stays is that the work happened — the steps in order, each one's kind, model, tool and cost, and how it ended — and the proof that none of it was changed.

That proof survives because the agents kit chains a record through the hashes of its words, not the words (The receipt and the record — what every piece of an agent's work leaves (Agents kit)). After an erasure VerifyTrace(task) answers Intact, with Erased saying how many lines no longer say what they said. A cost, a tool or a step changed afterwards still breaks its line's hash, and a sentence written into an erased line is refused by name.

On the meter, an erasure takes the person off: each row loses who it was for and the requester: line of what it was charged to; what was counted against a budget of their own is deleted. The rows, and what they cost, stay counted for the app.

Who erases?#

A request's own run, as the app itself. The agents kit's erasure functions ask the app's ErasesAgentRecords policy, which is nobody's until the app says otherwise — and it need not say. A person who manages privacy and audits agents is still refused when they call one by hand.

What about work still running, a hold, a restriction?#

  • Still running — the request waits ("an AI agent is still working on something for you or about you") and is asked again by the daily purge. A model call still on its way holds llm.calls the same way.
  • A legal hold (Legal holds — nothing a dispute needs is erased or purged while it lasts) — a held person's work, and work marked with a held address, is neither erased nor purged while it stands.
  • A restriction — their work, and their rows of the meter, leave every ordinary read until it is lifted.
  • Every day, for everybody — content past its period is emptied, work past the record's period is deleted whole, and a call past its period loses who it was for.

Examples#

app Library {
  use Osysharp.Identity;
  use Osysharp.WorkQueues;
  use Osysharp.Mail;
  use Osysharp.Privacy;
  use Osysharp.Agents;
  use Osysharp.Agents.Testing;
  use Osysharp.Llm;
  // Both arrive by themselves under `osy lock`; this example is one file, so it names them.
  use Osysharp.Privacy.Agents;
  use Osysharp.Privacy.Llm;
  use Osysharp.Workflow;
  use Osysharp.Observability;
  use Osysharp.Storage;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Identity;
using Osysharp.Llm;
using Osysharp.Privacy;
using Osysharp.Privacy.Agents;

app.Llm = new LlmSetup {
  Models = [ new LlmModel { Name = "Everyday", Client = new ScriptedModel { Name = "Everyday" }, Model = "scripted-everyday",
                            Price = new LlmPrice { Input = 1, Output = 5 }, MaxOutputTokens = 1000 } ],
};
app.Agents = new AgentsSetup { Agents = [new Desk()], TaskBudget = 0.05m };

[Role] enum LibraryRole { Authenticator, Admin, Librarian, Member, Desk }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsAdmin     => RoleGrant.Any(g => g.Member == user && g.Role == LibraryRole.Admin);
policy IsLibrarian => RoleGrant.Any(g => g.Member == user && g.Role == LibraryRole.Librarian);
policy IsMember    => RoleGrant.Any(g => g.Member == user && g.Role == LibraryRole.Member);
policy IsDesk      => RoleGrant.Any(g => g.Member == user && g.Role == LibraryRole.Desk);

// Privacy's questions, and the Inbox's (a request is inbox work).
policy ManagesPrivacy   => IsAdmin;
policy PlacesLegalHolds => IsAdmin;
policy WorksInbox => IsAdmin;
policy LeadsInbox => IsAdmin;
policy AdministersInbox => IsAdmin;
policy ManagesOrganizations => IsAdmin;
policy CollaboratesOnRecords => IsAdmin;
// The agents' questions. Nobody is named to erase their records: a privacy request's own run does.
policy UsesAgents       => IsMember || IsLibrarian;
policy SupervisesAgents => IsLibrarian;
policy AuditsAgents     => IsAdmin;
policy ReadsModelUsage   => IsAdmin;
policy ManagesModelSpend => IsAdmin;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}

/// One book out on loan.
entity Loan {
  [Required, MaxLength(200)] string Title;
  Person? Borrower;
  [MaxLength(320)] string BorrowerEmail = "";
  int Renewals = 0;
  security {
    allow read, update when IsMember where Borrower == user;
    allow read, update, create when IsLibrarian || IsDesk;
  }
}

/// Give a loan three more weeks.
[Action("Renew a loan")]
public string RenewLoan(Loan loan) { loan.Renewals = loan.Renewals + 1; return $"“{loan.Title}” is renewed."; }

/// What the desk hands back.
public class Renewed { public int Loans; public string Said = ""; }

/// Renews what a member asks to have renewed.
public class RenewLoans : Skill<string, Renewed> {
  public override string Description => "Renew the loans a member asks about.";
  public override List<ActionInfo> Tools => [Actions.Of(RenewLoan)];
}

/// The library's desk.
[Actor(Roles = [LibraryRole.Desk])]
public class Desk : Agent {
  public static Person Principal => new Person { Email = "[email protected]", DisplayName = "The desk" };
  public override string Purpose => "You are the library's desk: you renew loans.";
  public override List<SkillBase> Skills => [new RenewLoans()];
  public override Permits May => Permits.ChangesAlone();        // it renews a loan by itself
}

/// A member asks the desk to renew a loan — work done FOR whoever is signed in.
public AgentOutcome<Renewed> AskTheDesk(string wish) {
  return await Agents.Of<Desk>().Do(new RenewLoans(), wish, about: "Renewals for " + Session.CurrentUser!.DisplayName);
}

[SuppressWarning("IMPORTED_ENTITY_HAS_NO_POLICY")] partial entity WorkItem { }

[TestFixture]
void Town() {
  var ada = new Person { Email = "[email protected]", DisplayName = "Ada" };
  var sol = new Person { Email = "[email protected]", DisplayName = "Sol" };
  var mia = new Person { Email = "[email protected]", DisplayName = "Mia Berg" };
  new RoleGrant { Member = ada, Role = LibraryRole.Admin };
  new RoleGrant { Member = sol, Role = LibraryRole.Librarian };
  new RoleGrant { Member = mia, Role = LibraryRole.Member };
  new Loan { Title = "The Dispossessed", Borrower = mia, BorrowerEmail = "[email protected]" };
  new AgentSeat { Agent = "Desk", Sponsor = sol };      // the librarian answers for the desk
}
principal Ada => Person.Single(p => p.Email == "[email protected]");
principal Mia => Person.Single(p => p.Email == "[email protected]");

[Test(Town)]
void The_chain_verifies_after_a_member_is_erased() {
  Guid task = Guid.Empty;
  Guid mia = Guid.Empty;
  runas (Mia) {
    mia = Session.CurrentUser!.Id;
    var loan = Loan.Single(l => l.Title == "The Dispossessed");
    var script = AgentScript.Of(
      Says.Tool(Actions.Of(RenewLoan), "{\"loan\":\"" + loan.Id.ToString() + "\"}"),
      Says.HandBack(new Renewed { Loans = 1, Said = "Mia, The Dispossessed is yours for three more weeks." }));
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
    task = AskTheDesk("Please renew The Dispossessed. Mia Berg, card 4471.").Task;
  }

  runas (Ada) {
    // Her copy would carry every word of it; a plan counts one piece of work in each category.
    var holder = new AgentsPersonalData();
    var subject = new DataSubject { Account = mia };
    Assert.Contains("card 4471", holder.Export(subject, CategoryOf("agents.trace")).Json);
    Assert.Equal(1, holder.Count(subject, CategoryOf("agents.record")));

    // She asks to be erased; Ada checked it is her, and approves the plan. The request's own run erases.
    var r = FileRequest(RequestKind.Erasure, mia, "", IdentityCheck.InPerson, "She asked at the desk with her library card.");
    ApproveRequest(r);
    Assert.Equal(RequestState.Done, PrivacyRequest.Single(x => x.Id == r.Id).State);
  }

  runas (Ada) {
    var receipt = AgentTask.Single(t => t.Id == task);
    // Nobody's, about nothing, and not a word of it left — for the auditor, who reads everything there is.
    Assert.Null(receipt.Requester);
    Assert.Equal("", receipt.About);
    Assert.Null(receipt.Result);
    Assert.Equal(0, TraceEvent.Count(e => e.Task.Id == task && e.Payload != null && e.Payload != ""));
    Assert.Equal(6, TraceEvent.Count(e => e.Task.Id == task && e.Summary == "Erased."));
    // What happened is still told, and the chain still verifies.
    Assert.Equal(["RenewLoan"], Trajectory.Of(task));
    Assert.Equal(400, receipt.SpentMicros);
    var check = VerifyTrace(receipt);
    Assert.True(check.Intact);
    Assert.Equal(6, check.Lines);
    Assert.Equal(6, check.Erased);
    // A line changed after the erasure is still found.
    var lines = TraceOf(receipt);
    lines[1].CostMicros = 1;
    Assert.Equal("Line 2 is not as it was written.", CheckChain(lines).Problem);
    lines = TraceOf(receipt);
    lines[0].Summary = "The desk was given nothing.";
    Assert.Equal("Line 1 was erased, and something was written in its place.", CheckChain(lines).Problem);
    // The model calls are still on the meter, on nobody's name.
    Assert.Equal(0, ModelCall.Count(m => m.Requester == mia));
    Assert.Equal(2, ModelCall.Count());
    // Nobody erased it by hand: Ada manages privacy and audits the desk, and is refused.
    Assert.Denied(() => ForgetAgentWork(receipt));
  }
  runas (Mia) { Assert.Equal(0, AgentTask.Count()); }
}
app Studio {
  use Osysharp.Identity;
  use Osysharp.WorkQueues;
  use Osysharp.Mail;
  use Osysharp.Privacy;
  use Osysharp.Assignments;
  use Osysharp.WorkRepositories;
  use Osysharp.Llm;
  // Both arrive by themselves under `osy lock`; this example is one file, so it names them.
  use Osysharp.Privacy.Assignments;
  use Osysharp.Privacy.WorkRepositories;
  use Osysharp.Workflow;
  use Osysharp.Observability;
  use Osysharp.Storage;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Assignments;
using Osysharp.Identity;
using Osysharp.Privacy;
using Osysharp.Privacy.Assignments;
using Osysharp.Privacy.WorkRepositories;
using Osysharp.WorkRepositories;

// Applications are kept six months after the hire closes; what an interviewer thought is read by fewer people.
app.WorkRepositories = new WorkRepositoriesSetup {
  Kinds = [ new KindRule { Kind = ItemKind.Document, RetainFor = TimeSpan.FromDays(180) },
            new KindRule { Kind = ItemKind.Finding, Restricted = true } ],
};
// A case about a candidate reads, once she is erased, as a case about "a former candidate".
app.AssignmentsPrivacy = new AssignmentsPrivacySetup { StandIn = "a former candidate", Rules = new StudioPrivacy() };

[Role] enum StudioRole { Authenticator, Admin, Manager, Interviewer }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsAdmin   => RoleGrant.Any(g => g.Member == user && g.Role == StudioRole.Admin);
policy IsManager => RoleGrant.Any(g => g.Member == user && g.Role == StudioRole.Manager);

// Privacy's questions, and the Inbox's (a request is inbox work).
policy ManagesPrivacy   => IsAdmin;
policy PlacesLegalHolds => IsAdmin;
policy WorksInbox => IsAdmin;
policy LeadsInbox => IsAdmin;
policy AdministersInbox => IsAdmin;
policy ManagesOrganizations => IsAdmin;
policy CollaboratesOnRecords => IsAdmin;
// The manager asks for the hire and oversees it. Nobody is named to erase: a privacy request's own run does.
policy OpensAssignments    => IsManager;
policy OverseesAssignments => IsManager;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}

/// The studio's own note of who a reference check is about.
entity Candidate {
  [Required, MaxLength(200)] string Name;
  [MaxLength(320)] string Email = "";
  Assignment? Check;
  security { allow read, create, update when IsManager || IsAdmin; }
}

/// The words in a case's text that say who it is about: the candidate's name, as the studio's note has it.
public class StudioPrivacy : AssignmentPrivacyRules {
  public override List<string> PersonalWords(Assignment a) {
    var c = Candidate.FirstOrDefault(x => x.Check == a);
    return c == null ? [] : [c!.Name, c!.Name.Split(" ")[0]];
  }
}

/// An application arrives — and the studio says whose data it is.
public RepositoryItem ReceiveApplication(Assignment hire, string name, string email, string text) {
  var item = AddItem(hire, new NewItem { Kind = ItemKind.Document, Title = "Application: " + name, Content = text });
  MarkItemAbout(item, new DataSubject { Emails = [email] });
  return item;
}

[SuppressWarning("IMPORTED_ENTITY_HAS_NO_POLICY")] partial entity WorkItem { }

[TestFixture]
void Office() {
  var ada = new Person { Email = "[email protected]", DisplayName = "Ada" };
  var hanna = new Person { Email = "[email protected]", DisplayName = "Hanna" };
  new RoleGrant { Member = ada, Role = StudioRole.Admin };
  new RoleGrant { Member = hanna, Role = StudioRole.Manager };
  new Candidate { Name = "Maja Okafor", Email = "[email protected]" };
}
principal Ada   => Person.Single(p => p.Email == "[email protected]");
principal Hanna => Person.Single(p => p.Email == "[email protected]");

Assignment Job(Guid id) { return Assignment.Single(a => a.Id == id); }
RepositoryItem Item(Guid id) { return RepositoryItem.Single(i => i.Id == id); }

[Test(Office)]
void A_candidate_who_withdraws_is_erased_and_the_other_application_goes_when_its_time_is_up() {
  TestClock.Set(new DateTime(2026, 3, 2, 10, 0, 0));
  Guid hire = Guid.Empty;
  Guid majas = Guid.Empty;
  Guid svens = Guid.Empty;
  runas (Hanna) {
    var a = OpenAssignment(new NewAssignment { Title = "Hire an illustrator", Goal = "An illustrator has accepted.", Criteria = ["The decision is recorded"] });
    hire = a.Id;
    majas = ReceiveApplication(a, "Maja Okafor", "[email protected]", "Ten years of picture books.").Id;
    svens = ReceiveApplication(a, "Sven Lind", "[email protected]", "Editorial work for two dailies.").Id;
  }

  // Maja withdraws and asks to be erased. The hire is open, and goes on.
  runas (Ada) {
    var r = FileRequest(RequestKind.Erasure, null, "[email protected]", IdentityCheck.KnownDetails, "She wrote from the address on her application.");
    ApproveRequest(r);
    Assert.Equal(RequestState.Done, PrivacyRequest.Single(x => x.Id == r.Id).State);
  }
  runas (Hanna) {
    // What her application said is gone; its place in the record is not.
    Assert.Equal("", CurrentOf(Item(majas)).Content);
    Assert.Equal("Document (erased)", Item(majas).Title);
    Assert.NotNull(Item(majas).ErasedAt);
    Assert.Equal(Hanna.Id, Item(majas).AddedBy!.Id);
    // The hire's activity no longer names her, and Sven's application is word for word as it was.
    Assert.False(Job(hire).Activity.Any(l => l.Said.Contains("Maja")));
    Assert.Equal("Editorial work for two dailies.", CurrentOf(Item(svens)).Content);
    Assert.Equal(AssignmentState.Open, Job(hire).State);

    MeetCriterion(Job(hire).Criteria.Single(), "We offer Sven the place.");
    CompleteAssignment(Job(hire), "Sven accepted.");
  }

  // Nobody asks. Six months and a few days after the hire closed, Sven's application reaches its time.
  TestClock.Set(new DateTime(2026, 9, 7, 10, 0, 0));
  runas (Ada) { PurgeNow(); }
  runas (Hanna) {
    Assert.Equal("", CurrentOf(Item(svens)).Content);
    Assert.Equal("It was kept as long as its kind is.", Item(svens).ErasedWhy);
  }
}

[Test(Office)]
void A_case_about_a_candidate_reads_as_about_a_former_candidate() {
  Guid check = Guid.Empty;
  runas (Hanna) {
    var a = OpenAssignment(new NewAssignment {
      Title = "Check Maja Okafor's reference", Goal = "We know what Maja's last studio thought.", Criteria = ["The reference is on file"] });
    check = a.Id;
    Candidate.Single(c => c.Name == "Maja Okafor").Check = a;
    // Said once, for the case: everything in it is about her.
    MarkAssignmentAbout(a, new DataSubject { Emails = ["[email protected]"] });
    AddItem(a, new NewItem { Kind = ItemKind.Evidence, Title = "Call with the last studio", Content = "They would hire Maja again." });
  }
  runas (Hanna) {
    MeetCriterion(Job(check).Criteria.Single(), "Maja's reference is on file.");
    CompleteAssignment(Job(check), "A good reference for Maja Okafor.");
  }

  runas (Ada) {
    var r = FileRequest(RequestKind.Erasure, null, "[email protected]", IdentityCheck.KnownDetails, "She wrote from the address on her application.");
    ApproveRequest(r);
    Assert.Equal(RequestState.Done, PrivacyRequest.Single(x => x.Id == r.Id).State);
  }

  runas (Hanna) {
    var a = Job(check);
    // Words where she was — the case still reads as a piece of work that was done.
    Assert.Equal("Check a former candidate's reference", a.Title);
    Assert.Equal("A good reference for a former candidate.", a.Outcome);
    Assert.Equal("a former candidate's reference is on file.", a.Criteria.Single().MetBy);
    Assert.Equal(AssignmentState.Completed, a.State);
    Assert.False(a.Activity.Any(l => l.Said.Contains("Maja")));
    // The item in its repository was nobody's to mark: it went with the case.
    Assert.Equal("", CurrentOf(RepositoryItem.Single(i => i.Assignment.Id == check)).Content);
  }
}

See also#

Related

The Privacy kit — erasure that keeps only what the company must or may

`Osysharp.Privacy` answers a person's GDPR request — access, portability, erasure, restriction — per category of what…

The receipt and the record — what every piece of an agent's work leaves (Agents kit)

Every piece of work leaves one receipt, `AgentTask` — who asked, the account it ran as, what it cost against its limit…

Writing an agent — a class with a purpose, skills and an account of its own (Agents kit)

An agent is a class of the `Osysharp.Agents` kit: what it is for, the skills it holds, what it may spend. It runs as an…

Assignments — a piece of work owned until its outcome is reached (Assignments kit)

An assignment of the `Osysharp.Assignments` kit says who asked, the outcome wanted and what must be true for it to be…

Work repositories — what an assignment knows, kept as items (WorkRepositories kit)

The `Osysharp.WorkRepositories` kit keeps an assignment's evidence, correspondence, findings, documents, proposals and…

Model spend — budgets, the meter and the price book (Llm kit)

The Llm kit keeps three things about what model calls cost. The METER: one `ModelCall` row for every attempt, with the…

personal data (Classification(Personal.X))

Mark a field as personal data once, on the property, with a level of the core `Personal` enum. From then on the audit…

Legal holds — nothing a dispute needs is erased or purged while it lasts

A legal hold in `Osysharp.Privacy` keeps everything about a person — or one case — from being erased or purged while a…

Agents (a class with a job, its own account, a budget and a record)

An agent is a class of the `Osysharp.Agents` kit: a purpose, the skills it holds and what it may spend. A skill is a…