docs+test: correct two over-claims and pin name escaping (TASK-2759)

Codex round 2, fresh angles.

P1, accepted — my own docs over-claimed. The README and both SKILL.md
copies said the name appears wherever agent actors appear, including "item
timelines". Comments, version snapshots and note/decision entries carry the
actor KIND and no name (that is the exempt set the plan named, and TASK-2760
files the comment half), so on a timeline only ACTIVITY entries show it. Both
now say which entries carry it and which read "Agent".

P2, accepted — the README's fallback was wrong in a way that mattered. When
nothing resolves a name, the CLI omits X-Pad-Agent entirely (client.go:1884),
so actorFromRequest records the write as "user": it is attributed to the
PERSON, not to a generic "agent". Verified both call sites rather than
reasoning from the label. The generic "agent" rows that do exist come from
pre-naming writes and from audit events logged without agentMeta.

P2, accepted — the name is attacker-influenced text and every test used
benign values, so a rewrite to {@html} would have passed. Added a markup
payload at two surfaces that build their labels through different paths,
asserting no element is created and the text survives intact.

Claude-Session: https://claude.ai/code/session_01Cpr3teiHHsgcTmg2xhHA86
This commit is contained in:
xarmian
2026-08-24 16:45:35 +00:00
parent d1c5c3976e
commit fa22b6680e
5 changed files with 38 additions and 6 deletions
+8 -4
View File
@@ -130,7 +130,7 @@ Agents load relevant conventions automatically, and every agent action is attrib
**Name your agents:**
By default an agent's writes show up as a generic `agent`. Give it a name and that name appears instead — in the activity feed's Live and Audit views, on the dashboard's recent activity, on item timelines, and in the admin audit log. With more than one agent working a project, that is the difference between "something automated touched this" and knowing which one.
An agent that identifies itself gets its name shown on its writes — in the activity feed's Live and Audit views, on the dashboard's recent activity, on item timeline *activity* entries, and in the admin audit log. With more than one agent working a project, that is the difference between "something automated touched this" and knowing which one.
Pad takes the first of these it finds:
@@ -142,11 +142,15 @@ Pad takes the first of these it finds:
# 2. Per-process, runtime-agnostic. Any harness can set it.
export PAD_AGENT=reviewer
# 3. Otherwise Pad detects the runtime it knows (Claude Code reports
# "claude-code"), and falls back to the generic "agent" if it can't.
# 3. Otherwise Pad detects the runtimes it knows Claude Code reports
# "claude-code" and that detected id is used as the name.
```
The name is rendered exactly as sent — Pad does not keep a list of approved names or rewrite what you choose.
**If none of the three produce a name, the write is not marked as an agent's at all** — it is recorded as the person whose credentials it used, which is the case the caveat below is about. The generic `agent` label you may see on older entries is a write that identified itself before Pad stored names, or an event type that records the actor without the name (workspace membership changes, sign-ins).
The name is rendered exactly as sent — Pad keeps no list of approved names, and does not re-case or rewrite what you choose.
Not every entry can show it. Comments, version snapshots, and implementation-note/decision entries record only *that* an agent acted, because the name is not stored on those rows — they still read `Agent`. Activity entries are the ones that carry it.
**What this does not claim.** The name is supplied by the client and self-declared, so it records honesty, not identity. From `ResolveAgentName`'s own contract in `internal/cli/agent_identity.go`:
+1 -1
View File
@@ -315,7 +315,7 @@ See the **Onboarding** entry under Natural Language Routing above — it branche
5. **Be conversational.** You're not a command executor. You're a project partner.
6. **Reference existing items.** Use `[[Item Title]]` links in content to connect items.
7. **Keep it practical.** Size each item so it's a single meaningful unit of work — what "meaningful" means depends on the workspace (one branch/PR for code, one interview round for hiring, one research question for research). Ideas should be actionable. Docs should be concise. Check the workspace's conventions for domain-specific sizing rules.
8. **Attribution matters.** Items and comments you create are stamped `created_by: agent` and `source: cli` automatically — but the agent half only works if the CLI can tell it is being run by an agent. It detects Claude Code on its own; under any other harness, set `PAD_AGENT=<name>` in the environment (or `agent_name` in `.pad.toml`) or your writes will be recorded as the human whose credentials you are using. Whatever you send is DISPLAYED verbatim wherever agent actors appear — the activity feed, the dashboard, item timelines — so a specific name (`reviewer`, `nightly-triage`) is more use to a reader than a generic client id. Note this is self-declared, not proof: it makes the trail honest, it does not make it verifiable, so never treat `created_by` on a comment as evidence that a human said something.
8. **Attribution matters.** Items and comments you create are stamped `created_by: agent` and `source: cli` automatically — but the agent half only works if the CLI can tell it is being run by an agent. It detects Claude Code on its own; under any other harness, set `PAD_AGENT=<name>` in the environment (or `agent_name` in `.pad.toml`) or your writes will be recorded as the human whose credentials you are using. Whatever you send is DISPLAYED verbatim on the surfaces that store it — the activity feed, the dashboard's recent activity, activity entries on an item's timeline, the admin audit log — so a specific name (`reviewer`, `nightly-triage`) is more use to a reader than a generic client id. Comments, versions and note/decision entries record only that an agent acted, not which one. Note this is self-declared, not proof: it makes the trail honest, it does not make it verifiable, so never treat `created_by` on a comment as evidence that a human said something.
9. **Follow project conventions.** Always load and follow active conventions before performing work. They are project-specific rules that override your defaults. When a role is active, load both role-specific and global conventions.
10. **Learn and teach.** When the user corrects your behavior or teaches you a project-specific rule, offer to save it as a convention: "Should I save this as a project convention so future agents follow it too?" Use `pad item create convention "Title" --field trigger=<inferred> --field scope=<inferred> --field priority=should --stdin` with an appropriate trigger inferred from the context. If the correction is role-specific, add `--field role=<slug>`.
11. **Role context is per-conversation.** If roles exist, ask which role the user is working as on first invocation. Remember it for the session. Auto-filter queries and suggest assignments accordingly. Never block on role — if the user says "no role" or the workspace has no roles, work normally.
+1 -1
View File
@@ -325,7 +325,7 @@ Run the **onboard** invokable playbook — see the **Onboarding** entry under Na
5. **Be conversational.** You're not a command executor. You're a project partner.
6. **Reference existing items.** Use `[[Item Title]]` links in content to connect items.
7. **Keep it practical.** Size each item so it's a single meaningful unit of work — what "meaningful" means depends on the workspace (one branch/PR for code, one interview round for hiring, one research question for research). Ideas should be actionable. Docs should be concise. Check the workspace's conventions for domain-specific sizing rules.
8. **Attribution matters.** Items and comments you create are stamped `created_by: agent` and `source: cli` automatically — but the agent half only works if the CLI can tell it is being run by an agent. It detects Claude Code on its own; under any other harness, set `PAD_AGENT=<name>` in the environment (or `agent_name` in `.pad.toml`) or your writes will be recorded as the human whose credentials you are using. Whatever you send is DISPLAYED verbatim wherever agent actors appear — the activity feed, the dashboard, item timelines — so a specific name (`reviewer`, `nightly-triage`) is more use to a reader than a generic client id. Note this is self-declared, not proof: it makes the trail honest, it does not make it verifiable, so never treat `created_by` on a comment as evidence that a human said something.
8. **Attribution matters.** Items and comments you create are stamped `created_by: agent` and `source: cli` automatically — but the agent half only works if the CLI can tell it is being run by an agent. It detects Claude Code on its own; under any other harness, set `PAD_AGENT=<name>` in the environment (or `agent_name` in `.pad.toml`) or your writes will be recorded as the human whose credentials you are using. Whatever you send is DISPLAYED verbatim on the surfaces that store it — the activity feed, the dashboard's recent activity, activity entries on an item's timeline, the admin audit log — so a specific name (`reviewer`, `nightly-triage`) is more use to a reader than a generic client id. Comments, versions and note/decision entries record only that an agent acted, not which one. Note this is self-declared, not proof: it makes the trail honest, it does not make it verifiable, so never treat `created_by` on a comment as evidence that a human said something.
9. **Follow project conventions.** Always load and follow active conventions before performing work. They are project-specific rules that override your defaults. When a role is active, load both role-specific and global conventions.
10. **Learn and teach.** When the user corrects your behavior or teaches you a project-specific rule, offer to save it as a convention: "Should I save this as a project convention so future agents follow it too?" Use `pad item create convention "Title" --field trigger=<inferred> --field scope=<inferred> --field priority=should --stdin` with an appropriate trigger inferred from the context. If the correction is role-specific, add `--field role=<slug>`.
11. **Role context is per-conversation.** If roles exist, ask which role the user is working as on first invocation. Remember it for the session. Auto-filter queries and suggest assignments accordingly. Never block on role — if the user says "no role" or the workspace has no roles, work normally.
@@ -78,6 +78,22 @@ describe('TimelineActivityCard agent name', () => {
expect(getByText('Agent')).toBeTruthy();
});
// Codex round 2. The name is attacker-influenced: it is whatever a client
// put in X-Pad-Agent, and the server stores it without inspection. Svelte
// text interpolation escapes it today, so this passes as written — the
// point is that it would STOP passing if any of these render paths were
// rewritten to `{@html}`, which is the plausible way an "allow rich agent
// labels" change would arrive.
it('renders a name containing markup as text, never as elements', () => {
const payload = '<img src=x onerror="alert(1)">';
const { container, getByText } = render(TimelineActivityCard, {
activity: activity({ metadata: JSON.stringify({ agent: payload }) })
});
expect(container.querySelector('img')).toBeNull();
expect(getByText(payload)).toBeTruthy();
});
it('never reads the stamp for a non-agent actor', () => {
// Guards against keying the chip on metadata alone. A human's write can
// carry an `agent` key — the merge in agentMeta is textual and this
@@ -141,6 +141,18 @@ describe('activity page — Audit view', () => {
expect(host.querySelector('.actor-badge.agent')!.classList.contains('named')).toBe(false);
});
// Codex round 2 — the same escaping claim at a second surface, because the
// two views build their labels through different code paths and a future
// `{@html}` would land in one of them, not both. See the twin case in
// timelineActivityAgentName for why this passes today.
it('renders a name containing markup as text, never as elements', async () => {
const payload = '<img src=x onerror="alert(1)">';
await mountPage('audit', [act({ metadata: JSON.stringify({ agent: payload }) })]);
expect(host.querySelector('img')).toBeNull();
expect(host.querySelector('.actor-badge.agent')!.textContent!.trim()).toBe(payload);
});
it('never reads the stamp for a human row', async () => {
// The badge for a human is keyed on actor_name/source, and an `agent`
// key can ride along on any row's shared metadata blob.