Lawrence as a first-class identity — authority it borrows, work it owns
An identity + access model where Lawrence holds no standing broad permissions: every run's authority is least-privilege, the read/write mechanisms enforce scope, and the work is always attributed to Lawrence — across all four ways it acts.
This page is a review surface — select any text to leave a comment; reactions and a sign-off / approval are supported (at the bottom of the page). Proposed options are recommendations, open to challenge.
Overview
Today Lawrence can do work on a matter, but it can't own it: when it acts it borrows a human lawyer's identity, so every task it touches is authored by, and assigned to, that lawyer. There is no "Lawrence" the platform can point at, and nothing that says "Lawrence did this." This work delivers both — a real identity to assign work to, and a clean answer to what Lawrence is allowed to do.
The organising principle, and the thing to agree first:
Lawrence holds no standing broad permissions. Every run executes under a least-privilege authority — borrowed from a human, minted for a single matter, or (rarely) an explicit narrow agent grant — and the read/write mechanisms enforce that scope. Attribution is separate from authority — it records who acted, independent of whose permissions the run borrowed.
What is settled vs. what is a product decision. The mechanism — recording an actor separately from the borrowed authority (KD4) — is a backend decision and is proposed here. Whether the actor Lawrence is always the displayed author, even when a human directly instructed the run, is not a backend decision — it carries UX and user-expectation weight ("Jane drafted this" vs. "Lawrence drafted this, on behalf of Jane") and needs Product & Design as co-primary reviewers. See KD4.
flowchart LR
subgraph run["Every Lawrence run has three separable parts"]
direction TB
A["1 Authority — whose permissions bound it
borrowed / minted / granted"]
S["2 Scope — the reachable boundary
enforced by the read/write mechanisms"]
T["3 Attribution — who acted = Lawrence
always, independent of authority"]
end
A --> S
classDef d fill:#eef1f6,stroke:#9aa3b2,color:#1f2430;
classDef hot fill:#fff,stroke:#B5491F,color:#1f2430,stroke-width:2px;
class S,T d; class A hot;
Why this framing, not "firm-scoped identity with access". It's tempting to give Lawrence a firm-scoped identity with broad access. But a firm-scoped identity made a participant on every matter would read every matter — the cross-matter leak we must avoid. The key realisation: isolation comes from per-run authority + scope enforcement, not from the identity's grants or how narrowly the identity is scoped. Because the identity itself carries no authority, a single global Lawrence identity is just as safe as a per-firm one (§2) — and simpler. Scoping the identity per firm/matter adds no isolation; only the per-run authority does.
Solution shape
Five blocks: agree the model and its use cases, then build the identity, the per-run authority, the provisioning, and the enforcement that makes "no ambient power" real.
| Block | Delivers | Reuses (merged today) | The new edge |
|---|---|---|---|
| §1 Model & use cases BE AI | One access model covering all four ways Lawrence acts (on/off a matter, with/without a human). | The per-matter permission cascade; the existing "act as a lawyer" JWT. | Naming authority vs attribution vs scope; the two no-human cases. |
| §2 Identity BE | A real, credential-less, global Lawrence identity — stable attribution with no firm link. | The credential-less getOrCreateFromEmail path; the AdminProfile side-table pattern; the @lawhive/access-control grants layer. | An AGENT identity type + agent profile. Firm attachment & standing assignability deferred (not needed by authority or attribution). |
| §3 Authority per run AI | Each run gets the right least-privilege authority: borrow / mint / grant. | signUserJwtForPerson (borrow a human's scope). | A matter-bound agent credential; the actor/on-behalf-of split in the agent. |
| §4 Provisioning BE | Lawrence exists as one global identity; not pre-added to firms or matters. | The credential-less creation path. | A one-line global seed — no per-firm or per-matter rows, no backfill. |
| §5 Enforcement BE | Reads & writes are bound to the run's authority — cross-matter is structurally impossible. | The matter gate (authorizeForMatter) and content scope checks. | Close the confused-deputy gap on the VFS internal read paths. |
1 · Use cases & the access model Backend Legal AI
Lawrence acts in four situations that differ on two axes: is there a human principal, and is the work scoped to one matter. A single model has to cover all four. The trick is to separate the three things an "identity" is usually asked to be at once:
- Authority (the principal) — whose permission-set bounds what the run can touch.
- Attribution (the actor) — who is recorded as having acted. The actor is always Lawrence, which is what makes "review everything Lawrence did" work no matter whose authority it ran under. Note this is distinct from the human principal the run borrowed from (§3): both are recorded, so even when a human directly instructs a run, the write is attributed to Lawrence with the instructing human retained as principal — "Lawrence, on behalf of Jane". Product Design own whether a given surface displays the actor (Lawrence), the principal (Jane), or both — see KD4.
- Scope — the reachable boundary, enforced by the read/write mechanisms (§5), never by trusting the request.
With that split, "does Lawrence just use the acting user's permissions?" has a precise answer: yes when there is a human principal (UC1, UC2); when there isn't (UC3, UC4) the authority is minted narrowly or granted explicitly — never standing or broad.
flowchart TD
R([New Lawrence run]) --> Q1{On behalf of a human?}
Q1 -->|yes| Q2{Scoped to one matter?}
Q1 -->|no| Q3{Scoped to one matter?}
Q2 -->|yes| UC1["UC1 · borrow the lawyer's authority,
bind to that matter"]
Q2 -->|no| UC2["UC2 · borrow the user's authority
their access, no matter narrowing"]
Q3 -->|yes| UC3["UC3 · mint a matter-scoped agent
capability from the trigger's matterId"]
Q3 -->|no| UC4["UC4 · explicit narrow agent grant
prefer: decompose into UC3 runs"]
classDef d fill:#eef1f6,stroke:#9aa3b2,color:#1f2430;
classDef warn fill:#fff,stroke:#c08552,color:#1f2430,stroke-width:2px;
class R,Q1,Q2,Q3,UC1,UC2,UC3 d; class UC4 warn;
| # | On behalf of | Matter | Authority source | Scope | Example |
|---|---|---|---|---|---|
| UC1 | a lawyer | one matter | Borrow the lawyer's JWT | matter gate ∩ the lawyer's access | a user directs Lawrence while on a matter |
| UC2 | a user | none | Borrow the user's JWT | the user's own access (0…N matters), no matter narrowing | a global (matter-less) task handed to Lawrence |
| UC3 | nobody | one matter | Mint a matter-scoped agent capability | that one matter | an event-triggered pipeline on one matter (e.g. a document arriving) |
| UC4 | nobody | none | Explicit narrow agent grant | the capability's boundary | a firm-level autonomous job |
- UC2 is safe even when the user can reach many matters — Lawrence can never exceed what that user could do themselves; the human's own access is the ceiling. It simply isn't matter-narrowed.
- UC4 is the only case where Lawrence carries authority of its own, and the riskiest. Recommendation: decompose firm-wide autonomous work into per-matter runs (UC3) so the blast radius stays per-matter; reserve a true standing grant for genuinely matter-less operations, make it an explicit least-privilege capability on the
AGENTidentity, and audit it. What UC4 work actually exists is OQ4.
2 · The identity & where it lives Backend
Updated after review (identity-service verified against origin/develop, 2026-07). Review challenged both the FirmMember modelling and the "must exist per firm" premise itself. Following that through: authority is per-run and attribution is global, so nothing forces a firm link — the recommendation is now a single global AGENT identity, with firm attachment + assignability deferred (below). Also corrects an assumption — the identity-service does not already have an agent model to reuse (no AGENT type, no AgentProfile); those must be built.
The model as it is (verified in identity-service / matter-service). An Identity is global — no firmId, no matterId. IdentityType has exactly one value, USER, hardcoded in createIdentity — no agent/bot/system type. Profile fields (firstName, lastName, imageUrl, contactEmail) live inline on Identity; the only profile side-table is AdminProfile — there is no AgentProfile or UserProfile. An identity joins a firm only via a FirmMember row (role ∈ ADMIN / LAWYER / SUPPORT), and a matter only via matter-service's MatterParticipant (keyed on personId; type LAWYER / TEAM_MEMBER / CLIENT). Two facts that decide this section: (a) assignability today is gated by assertFirmMembership — assignTask throws FORBIDDEN without a FirmMember row, and there is no getAssignableIdentities abstraction; (b) the per-resource grants layer (@lawhive/access-control: a grants[] keyed on identityId, authorizeForMatter, signUserJwtForPerson) already exists, but signUserJwtForPerson requires a FirmMember + Clerk credential and there is no agent/bot JWT minting yet.
erDiagram
Identity ||--o| AdminProfile : "humans (exists today)"
Identity ||--o| AgentProfile : "NEW - mirrors AdminProfile"
Identity ||--o{ FirmMember : "humans join firms"
Firm ||--o{ FirmMember : "has human members"
Matter ||--o{ MatterParticipant : "has participants (human)"
Identity {
string id "pers_ global - ONE Lawrence, all firms"
enum type "USER today; add AGENT"
string firstName
string lastName
relation credentials "0..1 Clerk; none for agents"
}
FirmMember {
string id "fmbr_ (humans only)"
enum role "ADMIN LAWYER SUPPORT - map to grants"
}
MatterParticipant {
string personId "soft-ref Identity"
enum participantType "LAWYER TEAM_MEMBER CLIENT"
}
Recommended: one global AGENT identity for Lawrence, not linked to any firm. No firm-link table is needed — not even for assignability (below).
The load-bearing decision: one global Lawrence, or one per firm? Recommendation: one global AGENT identity, not linked to a firm.
Why the firm link isn't needed. Test it against the three jobs an identity does. Authority is per-run — borrowed from a human's JWT (UC1/UC2), minted from the trigger's matterId (UC3), or resolved from the AGENT identity's own grants (UC4); none reads a Lawrence↔firm link. Attribution binds to the global identityId, so "Lawrence did this" reads back correctly in every firm without one. The only thing that ever pulls in a firm is standing assignability — the firm-scoped assign gate (assertFirmMembership) — and that is not a confirmed product need (see OQ8).
Correcting an earlier claim. A per-firm (or per-matter) identity does not add an isolation layer: the actor identity is used for write attribution, not read authorization, so it never sits on the path a cross-matter leak would take. Isolation comes entirely from per-run authority (KD0). So per-firm buys no security — only, potentially, a home for per-firm agent config later.
Why global is the safe default. Distinguish two "add it later"s: teaching the assign guard to accept an agent later (a one-line predicate change, keeping the one global identity) is cheap and additive; splitting a global identity into per-firm identities later is expensive (every historical attribution row references the one pers_ id → a re-attribution backfill). Choosing global now keeps every firm-shaped option open at additive cost, and avoids building scope for functionality that isn't confirmed. See KD1.
Deferred — designed, not built (a scope cut, like UC4). Standing assignability (Lawrence as a task/key-date assignee). Verified in code: assignTask only writes an assignedIdentityId and pings a notification — the assertFirmMembership call is an integrity guard ("assign a firm's task only to a firm member"), not an access mechanism. Being assigned confers no access: assignedIdentityId never appears in getMatterPermissions / @lawhive/access-control. So no firm link is ever needed — if assignability is confirmed, the fix is a one-line guard change ("an AGENT identity is a valid assignee"), plus handling that notifyTaskAssigned is a no-op for a credential-less agent. The old "FirmMember row vs dedicated link" question is therefore moot — neither is required. (A FirmMember row would be wrong regardless: its roles, e.g. ADMIN→can_view_all_matters, aren't grant-neutral.)
Identity across all firms — not per-firm, not per-matter, not firm-linked. Authority (per-run) and attribution (global identityId) need nothing more. Keeps all firm-shaped work additive and deferrable. See KD1.AGENT to IdentityType (today only USER) and an agent profile side-table mirroring the existing AdminProfile pattern (there is no AgentProfile today). This tells Lawrence apart in every "who did this" surface and the UI, and anchors any UC4 grant. See KD2.getOrCreateFromEmail already builds identities with no Credential); no Clerk credential is minted for an agent. Caveat: that path still requires a contactEmail today — decide whether to relax it for agents. See KD3.3 · Authority per run — borrow, mint, or grant Legal AI
Each run picks up its authority in one of three ways, matching the use cases in §1. Only the first exists today.
Borrow a human's authority (UC1, UC2)
The agent already has one field — the request's userId — that today means two things at once: the human we act on behalf of (drives read scope) and the actor writing to the VFS. To credit Lawrence while keeping the human's scope, split them: userId stays the on-behalf-of principal (reads run under the human's JWT via the existing signUserJwtForPerson), and a new actor_identity_id = Lawrence drives write attribution.
flowchart TB
subgraph nowb["Today — one field, two meanings"]
U1["request.userId"] --> C1["read scope"]
U1 --> W1["writes: author / assignee"]
end
subgraph nextb["Proposed — split"]
U2["userId = principal
borrowed authority, read scope"] --> C2["reads run as the human"]
LA["actor_identity_id = Lawrence"] --> W2["writes attributed to Lawrence"]
end
nowb --> nextb
classDef d fill:#eef1f6,stroke:#9aa3b2,color:#1f2430;
classDef n fill:#fff,stroke:#5a8a64,color:#1f2430,stroke-width:2px;
class U1,C1,W1,U2,C2 d; class LA,W2 n;
UC1 narrows this to one matter (the run's matterId); UC2 (matter-less global task) runs under the same borrowed authority with no matter narrowing — safe because the human's own access is the ceiling.
Mint a matter-scoped agent capability (UC3)
Autonomous work on a matter — an event-triggered pipeline, e.g. reacting to a document being added — has no human to borrow from. Mirror the human path with an agent one: a signAgentJwtForMatter(lawrenceId, matterId) that issues a token scoped to exactly one matter. The authority to mint it comes from the triggering event's matterId (ingestion says "matter X"), so the pipeline can only ever obtain a matter-X token — the scope is baked into the token and can't be widened by anything the model emits.
An explicit, narrow agent grant (UC4) — designed now, built later
Genuinely matter-less autonomous work (no human, no matter) is the only case where Lawrence needs authority of its own. How grants would be provided: attach explicit, named capabilities to the AGENT identity through the existing RBAC grant system — the same machinery that carries firm grants like "view all matters" today, but as a small, deliberately-narrow agent-capability set (e.g. "create a firm-level task"), never a blanket grant. When a run has no principal and no matter, its authority is resolved from those grants and audited on use. Prefer to avoid UC4 entirely by decomposing firm-wide work into per-matter UC3 runs, which keeps the blast radius per-matter.
Deferred — designed, not implemented now (a deliberate scope cut). No confirmed UC4 work exists yet, and building the standing agent-grant path would enlarge scope for no near-term payoff. The initial build implements only borrow (UC1/UC2) and mint (UC3); the UC4 grant mechanism is specified here so the model is complete and forward-compatible, and is built only when a real matter-less, human-less task appears (tracked at OQ4).
How access is stored & provided
All three sources converge on the one thing the platform already resolves — a grants[] list keyed on an identityId, and the resolver never inspects whether that identity is human (which is what makes an agent principal cheap to add). The load-bearing fact: there is no grant store. Access is either derived from a role via static in-code matrices, or carried in a signed token — nothing else exists, and nothing else needs to.
| Authority | Where the access lives | How the run obtains it | How it's validated |
|---|---|---|---|
| Borrow (UC1/UC2) | The human's existing role rows (firm membership / matter participant / team) → grants via the matrices | Mint a person-impersonation token carrying the human's identity (exists today) | The service resolves grants from the human's identityId, exactly as if they had called |
| Mint (UC3) | Nowhere — not stored. The access rides in the signed token itself | A trusted service mints an agent-matter token { agentId, matterId, grants }, only when the trigger carries that matterId | The matter gate short-circuits: the token's matterId must equal the requested matter, else deny |
| Grant (UC4) | A small, deliberately-narrow agent role → grants matrix on the AGENT identity — a new per-resource table alongside the existing FIRM_ROLE_GRANTS / skill / template matrices in @lawhive/access-control | Resolved at authorization time from the agent's role — no token needed | The existing buildPermissionResult resolver (keyed on identityId) produces the grant list, exactly as for humans |
So the new surface area stays additive: for mint, a new agent-token type + a matter-scope short-circuit in the resolver (it already keys on identityId and returns a grants[], so callers can't tell an agent principal apart); for grant, a new agent-permissions matrix in the same shape as the existing role→grants tables. This keeps UC4's grants on the AGENT identity itself — not smuggled in via a human FirmMember role (whose grants aren't neutral; see §2).
4 · Provisioning Backend
Because authority is borrowed or minted per run and the identity is global (§2), provisioning is trivial — no per-firm rows, no per-matter rows. Lawrence just needs to exist, once.
Identity (type: AGENT) + its agent profile, credential-less, idempotent. That's the whole standing footprint — no FirmMember row, no firm link of any kind, no matter participation. Authority and attribution need nothing more.Reversal from the earlier drafts: both the original "Lawrence participant on every matter" plan and the interim "one identity + firm link per firm" plan are dropped. Standing matter participation was the cross-matter leak; the per-firm identity was scope for unconfirmed assignability (§2). Matter reach is per-run (UC1/UC3); firm attachment is deferred until a real need appears.
5 · Enforcement & isolation Backend
The model only holds if the mechanisms actually enforce each run's scope. Two facts from the code:
- Assignment is an integrity guard, not an access grant.
assignTaskwrites anassignedIdentityIdand callsassertFirmMembership— which throwsFORBIDDENfor a non-member — but that guard only enforces "assign a firm's task to a firm member"; it does not grant access. Confirmed:assignedIdentityIdis read only for display, the "my tasks" filter, and notifications — never ingetMatterPermissions/@lawhive/access-control. So a global, firm-less Lawrence simply isn't a standing assignee under today's guard — which is fine for this build: Lawrence acts (work attributed to it), needing no firm link. If being assigned tasks is later confirmed (OQ8), the fix is a one-line guard change ("anAGENTidentity is a valid assignee"), not a firm link — no human firm-role is ever borrowed. - Matter reads are per-matter gated — except on the paths the agent uses. A human's access to a matter is resolved per-matter (participant / team / firm-admin), enforced by
authorizeForMatter. But the VFS read paths for tasks, messages, participants and document metadata go throughinternalprocedures that trust the request'smatterIdand don't re-check the acting identity. Notes and document content are properly gated; those others are not.
That second point is a confused-deputy gap: a prompt-injected instruction that got the agent to emit a path with a different matterId would have those internal paths serve another matter's data. Closing it is a required security fix, not a nicety.
flowchart TB
subgraph now["Today — VFS internal read paths"]
r1["read tasks / messages / metadata"] --> i1["internal procedure:
trusts request matterId,
no identity gate"] --> x1["serves whatever matter was asked"]
end
subgraph next["Hardened"]
r2["read any path"] --> g2{"principal authorized
for THIS matter?"}
g2 -->|yes| ok["serve, bound to the run's matter"]
g2 -->|no| deny["deny"]
end
now --> next
classDef d fill:#eef1f6,stroke:#9aa3b2,color:#1f2430;
classDef n fill:#fff,stroke:#5a8a64,color:#1f2430,stroke-width:2px;
classDef bad fill:#fff,stroke:#a83838,color:#1f2430;
class r2,g2,ok d; class i1,x1 bad; class r1 d;
lawyer-style) procedures, or add authorizeForMatter, so the principal's access to the requested matter is checked every time — not trusted from the path.6 · Key design decisions
Genuine forks only. Proposed = recommendation, open to challenge.
| # | Decision | Proposed | Alternatives | Why |
|---|---|---|---|---|
| KD0 | Access model | No standing broad permissions: per-run authority (borrow / mint / grant) + scope enforced by the mechanisms; the actor is always Lawrence (how it's displayed is a product decision — KD4) | A firm-scoped identity with standing access; per-matter identities | Handles all four use cases with least privilege and no cross-matter leak. A standing-access identity re-introduces the leak; per-matter identities look like static isolation but give none here — the actor identity isn't on the read-authorization path (scope rides on the token/grants), so they only proliferate rows. |
| KD1 | Identity scope BE | One global AGENT identity for Lawrence — not linked to a firm. Firm attachment + standing assignability deferred (added additively if a real need appears). (Revised twice after review: "FirmMember row" → "distinct agent + firm link" → "global identity, no link".) | Per firm — via a FirmMember row (grants aren't neutral — §2) or a dedicated firm-link table; per matter (row proliferation, no added isolation) | Authority is per-run and attribution binds to the global identityId, so neither needs a firm link — nothing does except standing assignability, which isn't confirmed (OQ8). Global keeps every firm-shaped option cheap and additive; splitting a global identity into per-firm ones later is the one costly migration (historical attribution → re-backfill), so pick global now. Per-firm/per-matter scoping of the identity adds no isolation (KD0). |
| KD2 | Agent type & profile | Add AGENT to IdentityType (today only USER) + an agent profile side-table mirroring AdminProfile | Reuse USER + a metadata flag; store nothing (profile fields inline) | A real type distinguishes Lawrence in "who did this" and the UI, and anchors any UC4 grant. Verified: no AGENT type and no AgentProfile exist today — both are new, but AdminProfile is the precedent to copy. No gate branches on type; and if standing assignability is ever needed it comes from a consumer change (deferred — §2), not the type. |
| KD3 | Credential story | Credential-less (no Clerk credential for agents) | A real service/Clerk credential | Lawrence has no login; the credential-less getOrCreateFromEmail path already builds identities with no Credential. Caveats: that path still requires a contactEmail today (relax for agents?), and signUserJwtForPerson requires a FirmMember+Clerk — so it can't serve agents, confirming UC3 needs its own token minting (KD5). |
| KD4 | Actor vs on-behalf-of BE Product Design | Mechanism (BE, proposed): split the agent's userId into principal (reads) + actor_identity_id (writes), recording both.Displayed-author policy (Product/Design, open): when a human directly instructs a run, is the surfaced author "Lawrence", "Jane", or "Lawrence on behalf of Jane"? Proposed default: attribute the write to Lawrence, retain the principal, and let each surface choose what to show. | Keep the single conflated field; or always display the human as author (Lawrence invisible); or always display Lawrence only. | Mechanism: the only way to credit Lawrence while preserving the human's read scope; keeping it conflated means Lawrence can never own anything. Policy: going from "Jane drafted this" to "Lawrence drafted this" changes user expectations and how authorship evolves over time — a product/UX call, not a schema one, so it can't be settled by BE/AI alone. |
| KD5 | Matter-scoped agent credential (UC3) | Build signAgentJwtForMatter, authority from the trigger's matterId | Reuse a human's JWT (impossible — no human); grant standing access | The only clean, least-privilege way to let a no-human pipeline touch exactly one matter. Needed first by event-triggered matter pipelines (e.g. document extraction). |
| KD6 | Confused-deputy hardening | Gate the VFS internal read paths on the acting principal's matter access; force the actor id | Leave internal paths trusting the request matterId | Without it, "no ambient power" is only aspirational — a prompt-injected matterId could exfiltrate another matter. Required, not optional. |
7 · Open questions
★ OQ8 is load-bearing — decide it first. The identity granularity (one global Lawrence vs per-firm) is the one choice in this plan that is expensive to reverse; everything firm-shaped hangs off it.
- ★ OQ8 — Is one global Lawrence the intended attribution model? Product BE The recommendation (KD1, §2) is a single global
AGENTidentity: authority is per-run and attribution binds to the globalidentityId, so nothing needs a firm link. This is worth confirming explicitly because it is the only decision here that is costly to undo — adding a per-firm link later is cheap and additive, but splitting one global identity into per-firm identities later means re-attributing every historical record. So: is "Lawrence" one actor firm-wide, or must attribution/config differ per firm? If one global actor is right, defer all firm-shaped work. - OQ1 — One Lawrence, or chat vs pipelines? Given a global identity (OQ8), do the chat agent and the AI pipelines share the one Lawrence identity (simple provenance: "Lawrence did it") or get distinct identities (finer attribution)? Attribution is separate from authority, so either works — this is about provenance granularity. Review pushed back that the more useful unit may be the run/session, not the identity: keep one identity, track session metadata independently — see OQ7.
- OQ2 — Standing capabilities for the granted case (UC4). With Lawrence a distinct global
AGENTholding no human firm-role, what standing capabilities — if any — does theAGENTidentity get for UC4 (a new, deliberately-narrowagent-permissionsmatrix)? (Standing assignability is deferred and needs no firm link — just a guard change; see §2 and OQ8.) - OQ3 — Discoverability of Lawrence's activity for audit. Per-record attribution already says who acted on each item. Do we also need a queryable index of Lawrence's actions (a per-matter marker, or derived from assignments / attribution records), or is per-record attribution enough for auditing what the agent has done? (A session-indexed model — OQ7 — would answer this directly; "
identityIdfiltering for now" was raised in review as a sufficient near-term stopgap.) - OQ4 — What UC4 work actually exists? Which real tasks are matter-less and have no human principal? If most "firm-wide" work can be decomposed into per-matter (UC3) runs, we may not need a standing agent grant at all in the near term.
- OQ5 — UC2 read scope. For a matter-less global task on behalf of a user who can access many matters, is "the user's full access, unnarrowed" acceptable, or should even UC2 be narrowed to the specific matters the task references?
- OQ6 — An ephemeral, per-session approval layer? Product Design The confused-deputy fix (KD6) is a structural gate: the code denies an out-of-scope
matterIdevery time, no human involved. A complementary — and orthogonal — layer is runtime human consent: for actions that are technically authorised but high-consequence (especially UC4, or an agent about to write/act autonomously), pause and ask a human "Approve agent to do X on Y? → Yes / Yes-and-remember / No", à la Claude Code's tool-permission prompts. The grant would be ephemeral (session-scoped, created at point of use, not a standing row). This is a different axis from KD6 (authorised-by-policy vs. authorised-by-human), not a competing fix, and is not designed here: open questions include where the prompt surfaces, how "remember" is scoped, what counts as "risky enough to ask", and what happens for UC3/UC4 runs where no human is present to approve. Raised by review; capture the layer, decide scope separately. - OQ7 — Is a session/run a first-class, queryable entity? Attribution records who acted (Lawrence); it does not group outputs by which run produced them. A second, orthogonal dimension — a
sessionId/threadIdstamped on every artefact a run creates — would let us ask causal, groupable questions: "this chat thread produced tasks A, B and note C", "this bad output came from run S — what else did S touch?". Sketch from review: a shared "agent-produced" trait across artefact types plus a global entity → thread index, rather than bespoke columns per table. This is a change-tracking / provenance model the plan does not currently scope; it subsumes the granularity question in OQ1 and is the strongest answer to the audit question in OQ3. Decide whether it is in scope now or a deliberate follow-up.
8 · Rollout
- Sequence. (1)
AGENTtype + agent profile + seed the one global identity; (2) actor/on-behalf-of split so writes credit Lawrence (UC1/UC2); (3) the confused-deputy hardening (required before any write-as-Lawrence ships); (4) the matter-scoped agent credential (UC3) when the first event-triggered matter pipeline needs it; (5) UC4 grant, and firm attachment/assignability, only if a real need is confirmed (OQ8). - Provision before you act. The one global identity must exist before anything references Lawrence — a single idempotent seed, no per-firm backfill.
- Idempotency. Firm provisioning and backfill must be safe to re-run.
- Hardening is a gate, not a follow-up. Assignment- and write-as-Lawrence must never be settable by prompt content, and internal reads must check the principal's matter access.
- Attribution verified. Every action Lawrence takes reads back as "Lawrence" in the audit trail and UI, regardless of whose authority the run borrowed.
9 · Execution & review plan
Atomic PRs, per-repo stacks (≤7). Sizes are review estimates, not measured.
Stack A — identity-service
| PR | Contents |
|---|---|
| A1 | AGENT IdentityType value + agent profile side-table mirroring AdminProfile (KD2); credential-less creation for agents, relaxing the contactEmail requirement if needed (KD3). |
| A2 | Seed the single global Lawrence identity (identity + agent profile), idempotent (KD1, §4). No firm link, no per-firm backfill. |
| A3 (deferred) | Only if standing assignability is confirmed (OQ8): change the assign guard to accept an AGENT identity as a valid assignee + handle notifyTaskAssigned for a credential-less agent. A one-line predicate, no firm link. Not in the initial build. |
Stack B — agent authority split (agents + lawrence-api)
| PR | Contents |
|---|---|
| B1 | Split the agent's identity field: userId = principal, new actor_identity_id = Lawrence (KD4). |
| B2 | Thread the actor id into the tasks:///keydates:// writes so creations/assignments credit Lawrence; force the actor id (KD6). |
| B3 | signAgentJwtForMatter (UC3): mint a matter-scoped agent token, authority from the trigger's matterId (KD5). Consumed by event-triggered matter pipelines (e.g. document extraction). |
Stack C — enforcement hardening (matter-service + lawrence-api)
| PR | Contents |
|---|---|
| C1 | Gate the VFS internal read paths (tasks/messages/participants/doc-metadata) on the acting principal's matter access — route through matter-gated procedures or add authorizeForMatter (KD6). |
| C2 | Bind every agent read/write to the run's single matterId; regression tests for the confused-deputy case (a swapped matterId is denied). |
Stack D — assignability & attribution
| PR | Contents |
|---|---|
| D1 | Expose Lawrence to the FE as an actor + "is an agent" badge on soft-reference surfaces. (Standing assignability — Lawrence as a task/key-date assignee — is deferred with A3 until confirmed: OQ8.) |
| D2 | Optional queryable index of Lawrence's activity for audit (OQ3), if we choose that route. |
Glossary
| Term | Meaning |
|---|---|
| Authority (principal) | Whose permission-set bounds a run. Borrowed from a human, minted for one matter, or an explicit agent grant. |
| Attribution (actor) | Who is recorded as having acted — always Lawrence. Separate from authority, so "review everything Lawrence did" works across all use cases. |
| Scope | The reachable data boundary, enforced by the read/write mechanisms (the matter gate + matterId binding), never by trusting the request. |
| UC1–UC4 | The four ways Lawrence acts: on a matter for a lawyer (UC1); off a matter for a user (UC2); on a matter for nobody (UC3); off a matter for nobody (UC4). |
| Identity / FirmMember / MatterParticipant | The global person/agent record (one global row for Lawrence); the human firm-membership join (role ADMIN/LAWYER/SUPPORT); the "who is on this matter" record. Lawrence is not a member, participant of any — it's one global identity with no firm link (assignability, if ever needed, is a guard change, not a link; §2, OQ8). |
| signUserJwtForPerson / signAgentJwtForMatter | Borrow a human's authority (exists); mint a matter-scoped agent authority (new, UC3). |
| Confused deputy | A trusted component (the agent) tricked into using its authority on the attacker's behalf — here, emitting another matter's matterId. Closed by gating the internal read paths. |