mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
fix(connections): address personal auth prompts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
621943a441
commit
7db053bef2
22 files changed
+39947
-18
No files matched your search
@@ -696,8 +696,10 @@ old creator-excluding behavior as canonical `not_creator`, legacy `board_only` r
|
||||
become `human_only`, and both are marked `legacy_inherited_restriction`. Resolved
|
||||
outcomes and resolver attribution are immutable.
|
||||
|
||||
An explicit named addressee and a company-configured cap may narrow the effective
|
||||
audience. A cap never widens the requested audience. Tool-action confirmations and
|
||||
An explicit named agent or user addressee and a company-configured cap may narrow
|
||||
the effective audience. Only the exact named addressee may resolve an addressed
|
||||
interaction; a human does not override a user addressee. A cap never widens the
|
||||
requested audience. Tool-action confirmations and
|
||||
other hard-governed action cards remain `human_only` (or move to the formal approval
|
||||
system) regardless of a requested open audience.
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "issue_thread_interactions" ADD COLUMN "addressee_user_id" text;--> statement-breakpoint
|
||||
CREATE INDEX "issue_thread_interactions_addressee_user_idx" ON "issue_thread_interactions" USING btree ("addressee_user_id");
|
||||
+39803
File diff suppressed because it is too large.
Load diff
+7
@@ -1583,6 +1583,13 @@
|
||||
"when": 1787264558023,
|
||||
"tag": "0227_third_devos",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 228,
|
||||
"version": "7",
|
||||
"when": 1787274518405,
|
||||
"tag": "0228_last_anthem",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -45,6 +45,7 @@ export const issueThreadInteractions = pgTable(
|
||||
summary: text("summary"),
|
||||
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id),
|
||||
addresseeAgentId: uuid("addressee_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
addresseeUserId: text("addressee_user_id"),
|
||||
createdByUserId: text("created_by_user_id"),
|
||||
resolvedByAgentId: uuid("resolved_by_agent_id").references(() => agents.id),
|
||||
resolvedByRunId: uuid("resolved_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
||||
@@ -72,5 +73,6 @@ export const issueThreadInteractions = pgTable(
|
||||
.where(sql`${table.idempotencyKey} IS NOT NULL`),
|
||||
sourceCommentIdx: index("issue_thread_interactions_source_comment_idx").on(table.sourceCommentId),
|
||||
addresseeAgentIdx: index("issue_thread_interactions_addressee_agent_idx").on(table.addresseeAgentId),
|
||||
addresseeUserIdx: index("issue_thread_interactions_addressee_user_idx").on(table.addresseeUserId),
|
||||
}),
|
||||
);
|
||||
@@ -207,7 +207,9 @@ export interface AttentionResolverAudience {
|
||||
resolverPolicyProvenance: IssueThreadInteractionResolverPolicyProvenance;
|
||||
/** Agent the card is addressed to, when it names one. */
|
||||
addresseeAgentId: string | null;
|
||||
/** Display name of {@link addresseeAgentId}, resolved server-side. */
|
||||
/** User the card is addressed to, when it names one. */
|
||||
addresseeUserId?: string | null;
|
||||
/** Display name of the agent addressee, resolved server-side. */
|
||||
addresseeName: string | null;
|
||||
/** Agent that created the card, excluded when the policy is `not_creator`. */
|
||||
createdByAgentId: string | null;
|
||||
|
||||
@@ -1347,6 +1347,7 @@ export interface IssueThreadInteractionBase extends IssueThreadInteractionActorF
|
||||
sourceCommentId?: string | null;
|
||||
sourceRunId?: string | null;
|
||||
addresseeAgentId?: string | null;
|
||||
addresseeUserId?: string | null;
|
||||
title?: string | null;
|
||||
summary?: string | null;
|
||||
status: IssueThreadInteractionStatus;
|
||||
|
||||
@@ -253,6 +253,7 @@ export type ConnectionTokenSubject = { type: "app" } | { type: "user"; userId: s
|
||||
|
||||
export const CONNECTION_RECOVERABLE_ERROR_CODES = [
|
||||
"user_authorization_required",
|
||||
"organization_authorization_required",
|
||||
"grant_audience_denied",
|
||||
"grant_revoked",
|
||||
"needs_reauthorization",
|
||||
|
||||
@@ -1247,6 +1247,7 @@ export const requestItemVerdictsResultSchema = z.object({
|
||||
const createIssueThreadInteractionCommon = {
|
||||
resolverPolicy: issueThreadInteractionResolverPolicySchema.optional(),
|
||||
addresseeAgentId: z.string().uuid().nullable().optional(),
|
||||
addresseeUserId: z.string().trim().min(1).nullable().optional(),
|
||||
};
|
||||
|
||||
export const createIssueThreadInteractionSchema = z.discriminatedUnion("kind", [
|
||||
|
||||
@@ -860,9 +860,23 @@ describeEmbeddedPostgres("attention service", () => {
|
||||
addresseeAgentId: reviewerId,
|
||||
payload: { version: 1, questions: [] },
|
||||
});
|
||||
await db.insert(issueThreadInteractions).values({
|
||||
id: randomUUID(),
|
||||
companyId,
|
||||
issueId,
|
||||
kind: "ask_user_questions",
|
||||
status: "pending",
|
||||
title: "User-addressed question",
|
||||
createdByAgentId: workerId,
|
||||
addresseeUserId: "board-user",
|
||||
requestedResolverPolicy: "human_only",
|
||||
effectiveResolverPolicy: "human_only",
|
||||
payload: { version: 1, questions: [] },
|
||||
});
|
||||
await agentService(db).pause(reviewerId);
|
||||
|
||||
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
|
||||
const otherUserFeed = await attentionService(db).list(companyId, { userId: "other-user" });
|
||||
const audienceByTitle = new Map(feed.items
|
||||
.filter((item) => item.sourceKind === "issue_thread_interaction")
|
||||
.map((item) => [item.subject.title, item.resolverAudience]));
|
||||
@@ -888,6 +902,11 @@ describeEmbeddedPostgres("attention service", () => {
|
||||
addresseeAgentId: reviewerId,
|
||||
addresseeName: "Reviewer",
|
||||
});
|
||||
expect(audienceByTitle.get("User-addressed question")).toMatchObject({
|
||||
addresseeUserId: "board-user",
|
||||
effectiveResolverPolicy: "human_only",
|
||||
});
|
||||
expect(otherUserFeed.items.some((item) => item.subject.title === "User-addressed question")).toBe(false);
|
||||
// Non-interaction rows carry no resolver policy at all.
|
||||
expect(feed.items.find((item) => item.sourceKind !== "issue_thread_interaction")?.resolverAudience)
|
||||
.toBeNull();
|
||||
|
||||
@@ -1523,6 +1523,19 @@ rl.on("line", (line) => {
|
||||
.rejects.toMatchObject({ status: 403, reasonCode: "grant_audience_denied" });
|
||||
expect(await db.select().from(toolRuntimeSlots).where(eq(toolRuntimeSlots.connectionId, localTool.connection.id))).toHaveLength(0);
|
||||
|
||||
await db.insert(connectionGrantMembers).values({
|
||||
companyId: company.id,
|
||||
grantId: organizationGrant.id,
|
||||
subjectType: "user",
|
||||
subjectId: "alice",
|
||||
});
|
||||
expect(await executeIdentity()).toBe("organization");
|
||||
|
||||
await db.delete(toolRuntimeSlots).where(eq(toolRuntimeSlots.connectionId, localTool.connection.id));
|
||||
await db.update(connectionGrants).set({ status: "revoked" }).where(eq(connectionGrants.id, organizationGrant.id));
|
||||
await expect(gateway.executeTool({ sessionToken: session.token, tool: tool.name, parameters: {} }))
|
||||
.rejects.toMatchObject({ status: 409, reasonCode: "organization_authorization_required" });
|
||||
|
||||
await db.delete(connectionGrantMembers).where(eq(connectionGrantMembers.grantId, organizationGrant.id));
|
||||
await db.update(toolConnections).set({ credentialPolicy: "per_user" }).where(eq(toolConnections.id, localTool.connection.id));
|
||||
await db.update(heartbeatRuns).set({ responsibleUserId: null }).where(eq(heartbeatRuns.id, run.id));
|
||||
@@ -1860,6 +1873,7 @@ rl.on("line", (line) => {
|
||||
continuationPolicy: "wake_assignee",
|
||||
requestedResolverPolicy: "human_only",
|
||||
effectiveResolverPolicy: "human_only",
|
||||
addresseeUserId: "carol",
|
||||
});
|
||||
expect(interaction!.payload).toMatchObject({
|
||||
prompt: `Connect your ${connection.name} account to continue`,
|
||||
|
||||
@@ -93,6 +93,7 @@ export function errorHandler(
|
||||
const workspaceRepairPreconditionFailure = details?.code === "workspace_repair_precondition_failed";
|
||||
const structuredConnectionError = new Set([
|
||||
"user_authorization_required",
|
||||
"organization_authorization_required",
|
||||
"grant_audience_denied",
|
||||
"grant_revoked",
|
||||
"needs_reauthorization",
|
||||
|
||||
@@ -4328,6 +4328,7 @@ export function issueRoutes(
|
||||
effectiveResolverPolicy: string;
|
||||
resolverPolicyProvenance?: string | null;
|
||||
addresseeAgentId?: string | null;
|
||||
addresseeUserId?: string | null;
|
||||
kind: string;
|
||||
status: string;
|
||||
payload?: unknown;
|
||||
@@ -11264,6 +11265,7 @@ export function issueRoutes(
|
||||
interactionStatus: interaction.status,
|
||||
continuationPolicy: interaction.continuationPolicy,
|
||||
addresseeAgentId: interaction.addresseeAgentId ?? null,
|
||||
addresseeUserId: interaction.addresseeUserId ?? null,
|
||||
requestedResolverPolicy: interaction.requestedResolverPolicy,
|
||||
effectiveResolverPolicy: interaction.effectiveResolverPolicy,
|
||||
resolverPolicyProvenance: interaction.resolverPolicyProvenance,
|
||||
|
||||
@@ -733,6 +733,7 @@ function interactionVerbs(kind: string, payload: Record<string, unknown>) {
|
||||
export function interactionResolverAudience(
|
||||
row: {
|
||||
addresseeAgentId: string | null;
|
||||
addresseeUserId?: string | null;
|
||||
createdByAgentId: string | null;
|
||||
requestedResolverPolicy: string;
|
||||
effectiveResolverPolicy: string;
|
||||
@@ -752,6 +753,7 @@ export function interactionResolverAudience(
|
||||
(row.effectiveResolverPolicySource ?? "requested") as IssueThreadInteractionEffectiveResolverPolicySource,
|
||||
resolverPolicyProvenance: provenance,
|
||||
addresseeAgentId: row.addresseeAgentId,
|
||||
addresseeUserId: row.addresseeUserId ?? null,
|
||||
addresseeName: row.addresseeAgentId ? agentName(row.addresseeAgentId) : null,
|
||||
createdByAgentId: row.createdByAgentId,
|
||||
createdByAgentName: row.createdByAgentId ? agentName(row.createdByAgentId) : null,
|
||||
@@ -1172,6 +1174,7 @@ export function attentionService(db: Db, serviceOptions: AttentionServiceOptions
|
||||
summary: issueThreadInteractions.summary,
|
||||
payload: issueThreadInteractions.payload,
|
||||
addresseeAgentId: issueThreadInteractions.addresseeAgentId,
|
||||
addresseeUserId: issueThreadInteractions.addresseeUserId,
|
||||
createdByAgentId: issueThreadInteractions.createdByAgentId,
|
||||
requestedResolverPolicy: issueThreadInteractions.requestedResolverPolicy,
|
||||
effectiveResolverPolicy: issueThreadInteractions.effectiveResolverPolicy,
|
||||
@@ -1207,8 +1210,9 @@ export function attentionService(db: Db, serviceOptions: AttentionServiceOptions
|
||||
: [];
|
||||
const companyAgentMap = new Map(companyAgentRows.map((agent) => [agent.id, agent]));
|
||||
const boardInteractionRows = interactionRows.filter((row) =>
|
||||
row.addresseeAgentId === null ||
|
||||
!evaluateAgentInvokability(companyAgentMap.get(row.addresseeAgentId), companyAgentRows).invokable
|
||||
(row.addresseeAgentId === null ||
|
||||
!evaluateAgentInvokability(companyAgentMap.get(row.addresseeAgentId), companyAgentRows).invokable)
|
||||
&& (row.addresseeUserId === null || row.addresseeUserId === options.userId)
|
||||
);
|
||||
const visibleInteractionRows = collapsePendingConfirmationsToNewest(boardInteractionRows);
|
||||
const [interactionIssueMap, interactionImageMap, interactionPlanDocumentMap] = await Promise.all([
|
||||
|
||||
@@ -12,6 +12,7 @@ function interaction(overrides: Record<string, unknown> = {}) {
|
||||
createdByUserId: null,
|
||||
sourceRunId: "run-1",
|
||||
addresseeAgentId: null,
|
||||
addresseeUserId: null,
|
||||
effectiveResolverPolicy: "anyone",
|
||||
...overrides,
|
||||
};
|
||||
@@ -50,6 +51,18 @@ describe("issue-thread interaction resolver audience", () => {
|
||||
expect(decision).toMatchObject({ allowed: false, code: "interaction_addressee_mismatch" });
|
||||
});
|
||||
|
||||
it("allows only the addressed user to resolve a user-addressed interaction", () => {
|
||||
expect(evaluateIssueThreadInteractionResolverAudience({
|
||||
actor: { type: "user", userId: "alice" },
|
||||
interaction: interaction({ addresseeUserId: "alice", effectiveResolverPolicy: "human_only" }),
|
||||
})).toMatchObject({ allowed: true, reason: "allow_addressee" });
|
||||
|
||||
expect(evaluateIssueThreadInteractionResolverAudience({
|
||||
actor: { type: "user", userId: "bob" },
|
||||
interaction: interaction({ addresseeUserId: "alice", effectiveResolverPolicy: "human_only" }),
|
||||
})).toMatchObject({ allowed: false, code: "interaction_addressee_mismatch" });
|
||||
});
|
||||
|
||||
it("requires run attribution for agents", () => {
|
||||
const decision = evaluateIssueThreadInteractionResolverAudience({
|
||||
actor: { type: "agent", agentId: "agent-1", runId: null },
|
||||
|
||||
@@ -46,6 +46,7 @@ export type IssueThreadInteractionResolverAudienceInput = {
|
||||
createdByUserId?: string | null;
|
||||
sourceRunId?: string | null;
|
||||
addresseeAgentId?: string | null;
|
||||
addresseeUserId?: string | null;
|
||||
effectiveResolverPolicy: IssueThreadInteractionResolverPolicy | string;
|
||||
resolverPolicyProvenance?: string | null;
|
||||
};
|
||||
@@ -180,6 +181,18 @@ export function evaluateIssueThreadInteractionResolverAudience(
|
||||
}
|
||||
|
||||
if (input.actor.type === "user") {
|
||||
if (
|
||||
input.interaction.addresseeUserId
|
||||
&& input.interaction.addresseeUserId !== input.actor.userId
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
effectiveResolverPolicy,
|
||||
status: 403,
|
||||
code: "interaction_addressee_mismatch",
|
||||
message: "Only the addressed user may resolve this issue-thread interaction",
|
||||
};
|
||||
}
|
||||
if (
|
||||
creatorExcluded
|
||||
&& input.interaction.createdByUserId === input.actor.userId
|
||||
@@ -204,7 +217,11 @@ export function evaluateIssueThreadInteractionResolverAudience(
|
||||
return {
|
||||
allowed: true,
|
||||
effectiveResolverPolicy,
|
||||
reason: input.interaction.addresseeAgentId ? "allow_human_override" : "allow_human",
|
||||
reason: input.interaction.addresseeUserId
|
||||
? "allow_addressee"
|
||||
: input.interaction.addresseeAgentId
|
||||
? "allow_human_override"
|
||||
: "allow_human",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -419,6 +419,7 @@ function isEquivalentCreateRequest(
|
||||
row.kind === input.kind
|
||||
&& row.requestedResolverPolicy === input.resolverPolicy
|
||||
&& (row.addresseeAgentId ?? null) === (input.addresseeAgentId ?? null)
|
||||
&& (row.addresseeUserId ?? null) === (input.addresseeUserId ?? null)
|
||||
&& row.continuationPolicy === input.continuationPolicy
|
||||
&& (row.idempotencyKey ?? null) === (input.idempotencyKey ?? null)
|
||||
&& (row.sourceCommentId ?? null) === (input.sourceCommentId ?? null)
|
||||
@@ -476,6 +477,7 @@ function hydrateInteraction(
|
||||
...row,
|
||||
idempotencyKey: row.idempotencyKey ?? null,
|
||||
addresseeAgentId: row.addresseeAgentId ?? null,
|
||||
addresseeUserId: row.addresseeUserId ?? null,
|
||||
status: row.status as IssueThreadInteraction["status"],
|
||||
continuationPolicy: row.continuationPolicy as IssueThreadInteraction["continuationPolicy"],
|
||||
resolverPolicy: requestedResolverPolicy,
|
||||
@@ -2277,6 +2279,10 @@ export function issueThreadInteractionService(db: Db, opts: IssueThreadInteracti
|
||||
});
|
||||
const normalizedData = { ...data, resolverPolicy: policy.requestedResolverPolicy };
|
||||
|
||||
if (normalizedData.addresseeAgentId && normalizedData.addresseeUserId) {
|
||||
throw unprocessable("An issue-thread interaction cannot address both an agent and a user");
|
||||
}
|
||||
|
||||
if (normalizedData.addresseeAgentId) {
|
||||
if (normalizedData.addresseeAgentId === actor.agentId) {
|
||||
throw unprocessable("Agents cannot address issue-thread interactions to themselves");
|
||||
@@ -2407,6 +2413,7 @@ export function issueThreadInteractionService(db: Db, opts: IssueThreadInteracti
|
||||
summary: data.summary ?? null,
|
||||
createdByAgentId: actor.agentId ?? null,
|
||||
addresseeAgentId: data.addresseeAgentId ?? null,
|
||||
addresseeUserId: data.addresseeUserId ?? null,
|
||||
createdByUserId: actor.userId ?? null,
|
||||
payload: data.payload,
|
||||
})
|
||||
|
||||
@@ -7216,6 +7216,11 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
|
||||
const [interaction] = existingInteraction
|
||||
? await db.update(issueThreadInteractions).set({
|
||||
status: "pending",
|
||||
requestedResolverPolicy: "human_only",
|
||||
effectiveResolverPolicy: "human_only",
|
||||
resolverPolicyProvenance: "explicit",
|
||||
effectiveResolverPolicySource: "requested",
|
||||
addresseeUserId: input.subjectUserId,
|
||||
payload,
|
||||
result: null,
|
||||
resolvedAt: null,
|
||||
@@ -7231,6 +7236,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
|
||||
effectiveResolverPolicy: "human_only",
|
||||
resolverPolicyProvenance: "explicit",
|
||||
effectiveResolverPolicySource: "requested",
|
||||
addresseeUserId: input.subjectUserId,
|
||||
idempotencyKey,
|
||||
sourceRunId: binding.actorType === "agent" ? input.actor.sessionId ?? null : null,
|
||||
title: "Connect your account",
|
||||
|
||||
@@ -2610,6 +2610,7 @@ export function createToolGatewayService(
|
||||
effectiveResolverPolicy: "human_only",
|
||||
resolverPolicyProvenance: "explicit",
|
||||
effectiveResolverPolicySource: "requested",
|
||||
addresseeUserId: userId,
|
||||
payload,
|
||||
result: null,
|
||||
resolvedAt: null,
|
||||
@@ -2632,6 +2633,7 @@ export function createToolGatewayService(
|
||||
title: `Connect your ${connection.name}`,
|
||||
summary: "Personal authorization is required before this run can continue.",
|
||||
createdByAgentId: session.agentId,
|
||||
addresseeUserId: userId,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
@@ -2667,7 +2669,7 @@ export function createToolGatewayService(
|
||||
eq(connectionGrants.status, "active"),
|
||||
)).limit(1);
|
||||
if (!grant) {
|
||||
throw new ToolGatewayHttpError(409, "Organization authorization is required", "user_authorization_required", {
|
||||
throw new ToolGatewayHttpError(409, "Organization authorization is required", "organization_authorization_required", {
|
||||
connectionId: connection.id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3612,10 +3612,11 @@ export function IssueThreadInteractionCard({
|
||||
: null;
|
||||
// P4: audit-visible distinction between agent and human resolution.
|
||||
const resolvedByAgent = Boolean(interaction.resolvedByAgentId);
|
||||
// P3: interactions directed at a specific agent addressee.
|
||||
const addresseeLabel = interaction.addresseeAgentId
|
||||
// Interactions can be directed at a specific agent or board user.
|
||||
const addresseeLabel = interaction.addresseeAgentId || interaction.addresseeUserId
|
||||
? resolveActorLabel({
|
||||
agentId: interaction.addresseeAgentId,
|
||||
userId: interaction.addresseeUserId,
|
||||
agentMap,
|
||||
currentUserId,
|
||||
userLabelMap,
|
||||
|
||||
@@ -163,6 +163,20 @@ describe("describeInteractionAudience", () => {
|
||||
expect(audience.label).toBe("Human only");
|
||||
});
|
||||
|
||||
it("names one addressed user instead of the whole board", () => {
|
||||
const audience = describeInteractionAudience({
|
||||
interaction: confirmation({
|
||||
addresseeUserId: "user-alice",
|
||||
requestedResolverPolicy: "human_only",
|
||||
effectiveResolverPolicy: "human_only",
|
||||
}),
|
||||
addresseeLabel: "Alice",
|
||||
});
|
||||
expect(audience.summary).toBe("Only Alice can respond.");
|
||||
expect(audience.shortSummary).toBe("Only Alice can respond");
|
||||
expect(audience.label).toBe("Addressed");
|
||||
});
|
||||
|
||||
it("explains a governed-action clamp", () => {
|
||||
const audience = describeInteractionAudience({
|
||||
interaction: confirmation({
|
||||
|
||||
@@ -125,6 +125,7 @@ export interface InteractionAudienceFacts {
|
||||
effectiveResolverPolicySource: IssueThreadInteractionEffectiveResolverPolicySource;
|
||||
resolverPolicyProvenance: IssueThreadInteractionResolverPolicyProvenance;
|
||||
hasAddressee: boolean;
|
||||
isUserAddressee?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +144,7 @@ export function describeInteractionAudience({
|
||||
interaction: IssueThreadInteraction;
|
||||
/** Display label of the creating actor, when known. */
|
||||
creatorLabel?: string | null;
|
||||
/** Display label of the named addressee agent, when the card has one. */
|
||||
/** Display label of the named addressee, when the card has one. */
|
||||
addresseeLabel?: string | null;
|
||||
}): InteractionAudienceDescription {
|
||||
return describeResolverAudience({
|
||||
@@ -152,7 +153,8 @@ export function describeInteractionAudience({
|
||||
requestedResolverPolicy: interaction.requestedResolverPolicy,
|
||||
effectiveResolverPolicySource: interaction.effectiveResolverPolicySource,
|
||||
resolverPolicyProvenance: interaction.resolverPolicyProvenance,
|
||||
hasAddressee: Boolean(interaction.addresseeAgentId),
|
||||
hasAddressee: Boolean(interaction.addresseeAgentId || interaction.addresseeUserId),
|
||||
isUserAddressee: Boolean(interaction.addresseeUserId),
|
||||
},
|
||||
creatorLabel,
|
||||
addresseeLabel,
|
||||
@@ -176,10 +178,13 @@ export function describeResolverAudience({
|
||||
const policy = facts.effectiveResolverPolicy;
|
||||
const requestedPolicy = facts.requestedResolverPolicy;
|
||||
const hasAddressee = facts.hasAddressee;
|
||||
const addressee = addresseeLabel?.trim() || "the addressed agent";
|
||||
const isUserAddressee = facts.isUserAddressee === true;
|
||||
const addressee = addresseeLabel?.trim() || (isUserAddressee ? "the addressed user" : "the addressed agent");
|
||||
const creator = creatorLabel?.trim() || "the agent that created it";
|
||||
|
||||
const summary = policy === "human_only"
|
||||
const summary = isUserAddressee
|
||||
? `Only ${addressee} can respond.`
|
||||
: policy === "human_only"
|
||||
? "Only a person on the board can respond — agents cannot resolve this card."
|
||||
: hasAddressee
|
||||
? `Only ${addressee} or a person on the board can respond.`
|
||||
@@ -189,7 +194,9 @@ export function describeResolverAudience({
|
||||
|
||||
// Same fact, fewer words: a collapsed row has to answer "is this mine to
|
||||
// decide?" in one glance, next to the buttons that act on the answer.
|
||||
const shortSummary = policy === "human_only"
|
||||
const shortSummary = isUserAddressee
|
||||
? `Only ${addressee} can respond`
|
||||
: policy === "human_only"
|
||||
? "Only the board can respond"
|
||||
: hasAddressee
|
||||
? `Only ${addressee} or the board can respond`
|
||||
@@ -228,9 +235,9 @@ export function describeResolverAudience({
|
||||
policy,
|
||||
requestedPolicy,
|
||||
// A named addressee owns the response, so the label must not read "Anyone"
|
||||
// while the sentence next to it names one agent. `human_only` still wins,
|
||||
// because an addressed agent cannot resolve a human-only card.
|
||||
label: policy !== "human_only" && hasAddressee
|
||||
// while the sentence next to it names one actor. `human_only` wins for an
|
||||
// agent addressee, while a user addressee is the narrower human audience.
|
||||
label: (policy !== "human_only" || isUserAddressee) && hasAddressee
|
||||
? "Addressed"
|
||||
: RESOLVER_POLICY_LABELS[policy],
|
||||
summary,
|
||||
@@ -259,7 +266,8 @@ export function describeAttentionResolverAudience(
|
||||
requestedResolverPolicy: audience.requestedResolverPolicy,
|
||||
effectiveResolverPolicySource: audience.effectiveResolverPolicySource,
|
||||
resolverPolicyProvenance: audience.resolverPolicyProvenance,
|
||||
hasAddressee: Boolean(audience.addresseeAgentId),
|
||||
hasAddressee: Boolean(audience.addresseeAgentId || audience.addresseeUserId),
|
||||
isUserAddressee: Boolean(audience.addresseeUserId),
|
||||
},
|
||||
creatorLabel: audience.createdByAgentName,
|
||||
addresseeLabel: audience.addresseeName,
|
||||
|
||||
Reference in new issue
Block a user