feat(privacy): add secret-redaction utility, scrub dashboard stopped-output - #191
Merged
Merged
Conversation
…output
Adds a dependency-free `redact()` utility (src/utils/redact.ts) that scrubs
likely secrets from free-form text in two layers:
1. literal masking of known secret values (e.g. credential-shaped env vars)
2. regex fallback for common secret shapes — vendor token prefixes, PEM
private-key blocks, JWTs, key=value pairs, Authorization: Bearer headers,
and connection-string passwords.
Wires it into readLastAssistantOutput() so the last-assistant snapshot the
dashboard persists to events.jsonl (and renders in the HTML) can no longer
carry a leaked token. Ported from a battle-tested sync hook; runs after any
analytics extraction so metrics are unaffected.
This is the missing primitive behind TeamAI's "counts only, no prompt text"
posture: with deterministic scrubbing in place, later features can safely
persist richer session content behind explicit opt-in.
Test Plan:
- npx tsc --noEmit
- npx vitest run (139 files, 1729 tests pass)
- npm run build
- real-CLI E2E: piped a Stop event with a fake ghp_ token through
`node dist/index.js hook-dispatch stop`; events.jsonl stoppedOutput came
out as `GH_TOKEN=<REDACTED:gh_tok>`, raw token absent.
Claude-Session: https://claude.ai/code/session_01GEV81Xj6mhPzSPsyBrDPSd
This was referenced Jul 16, 2026
3 tasks
Eyre921
added a commit
to Eyre921/teamai-cli
that referenced
this pull request
Jul 17, 2026
…est feed
Implements Features 1 & 5 from docs/designs/team-intelligence-platform.md,
which were accepted ("ENG LOCKED") but never built — digest.ts's
getRecentSessions() has been reading an always-empty sessions/ directory.
`teamai session save` folds the dashboard's existing per-session event stream
(tool sequence, prompt turns, interventions) into a compact markdown summary and
appends it to a local monthly log (~/.teamai/session-logs/<year-month>.md). With
`--push`, a "valuable" session (interventions or substantial tool use, per
Decision 8) is committed to the team repo at sessions/<user>/<month>.md — the
path digest already reads, so "Session Highlights" lights up with no change to
digest.ts.
Reuses existing infrastructure rather than a new collection path:
- dashboard-collector readEvents() + aggregateSessionMetrics() for the data
- utils/git pushRepoDirectly() for the direct team commit (no MR), like contribute
- redact() (from Tencent#191) on the only free text included (the first-prompt line)
Review feedback addressed:
- Placed under a `session` subcommand group (`teamai session save`) instead of a
top-level verb, matching how `roles` / `source` are organized, leaving room
for `session list` / `session show` later.
- Team upload defaults to counts + tools only; the redacted first-prompt line is
opt-in via `--include-prompt`, since redact() is best-effort. Local logs keep it.
Team upload is opt-in; local logs are pruned after 90 days (Feature 5 retention).
Test Plan:
- npx tsc --noEmit
- npx vitest run (142 files, 1759 tests; 14 in session-collector.test.ts)
- npm run build
- real-CLI E2E: seeded ~/.teamai/dashboard/events.jsonl, ran
`node dist/index.js session save --session-id <sid>`; wrote
~/.teamai/session-logs/2026-07.md with folded stats, a `ghp_` token in the
prompt came out `<REDACTED:gh_tok>`; old `save-session` now errors as unknown;
`--include-prompt` appears in `session save --help`.
Claude-Session: https://claude.ai/code/session_01GEV81Xj6mhPzSPsyBrDPSd
Eyre921
added a commit
to Eyre921/teamai-cli
that referenced
this pull request
Jul 21, 2026
…est feed
Implements Features 1 & 5 from docs/designs/team-intelligence-platform.md,
which were accepted ("ENG LOCKED") but never built — digest.ts's
getRecentSessions() has been reading an always-empty sessions/ directory.
`teamai session save` folds the dashboard's existing per-session event stream
(tool sequence, prompt turns, interventions) into a compact markdown summary and
appends it to a local monthly log (~/.teamai/session-logs/<year-month>.md). With
`--push`, a "valuable" session (interventions or substantial tool use, per
Decision 8) is committed to the team repo at sessions/<user>/<month>.md — the
path digest already reads, so "Session Highlights" lights up with no change to
digest.ts.
Reuses existing infrastructure rather than a new collection path:
- dashboard-collector readEvents() + aggregateSessionMetrics() for the data
- utils/git pushRepoDirectly() for the direct team commit (no MR), like contribute
- redact() (from Tencent#191) on the only free text included (the first-prompt line)
Review feedback addressed (round 1):
- Placed under a `session` subcommand group (`teamai session save`) instead of a
top-level verb, matching how `roles` / `source` are organized, leaving room
for `session list` / `session show` later.
- Team upload defaults to counts + tools only; the redacted first-prompt line is
opt-in via `--include-prompt`, since redact() is best-effort. Local logs keep it.
Review feedback addressed (round 2):
- Fixed user-facing command strings that still said `teamai save-session` (an
unknown command): the read-only error hint and the retry hint now say
`teamai session save --push`; aligned docstrings in save-session.ts /
session-collector.ts / types.ts and the design-doc references.
- `assertNotReadOnly` on the --push path is now wrapped in try/catch: an
HTTP-mode (read-only) team prints a friendly message and keeps the local log
instead of throwing an uncaught stack trace.
- Idempotency key is now the full session id in a non-rendering HTML comment
(`<!-- teamai:session <id> -->`) rather than the 8-char short id, removing the
~1-in-4B same-month prefix collision that could silently drop a session.
- Replaced the ad-hoc `Promise.race`/`setTimeout` push guard with the shared
`withTimeout()` helper (src/utils/async.ts, from Tencent#211), which clears its timer
in a finally so the process doesn't linger after a fast push.
Team upload is opt-in; local logs are pruned after 90 days (Feature 5 retention).
Test Plan:
- npx tsc --noEmit
- npx vitest run (145 files, 1801 tests; 15 in session-collector.test.ts,
incl. a new "distinct sessions sharing an 8-char id prefix" case)
- npm run build
- real-CLI E2E: seeded ~/.teamai/dashboard/events.jsonl, ran
`node dist/index.js session save --session-id <sid>`; wrote
~/.teamai/session-logs/2026-07.md with folded stats + full-id marker, a `ghp_`
token in the prompt came out `<REDACTED:gh_tok>`, a second run was idempotent;
`--push` against a read-only HTTP team printed the graceful message (exit 0, no
stack trace); old `save-session` errors as unknown; `--include-prompt` appears
in `session save --help`.
jeff-r2026
pushed a commit
that referenced
this pull request
Jul 21, 2026
…est feed (#192) Implements Features 1 & 5 from docs/designs/team-intelligence-platform.md, which were accepted ("ENG LOCKED") but never built — digest.ts's getRecentSessions() has been reading an always-empty sessions/ directory. `teamai session save` folds the dashboard's existing per-session event stream (tool sequence, prompt turns, interventions) into a compact markdown summary and appends it to a local monthly log (~/.teamai/session-logs/<year-month>.md). With `--push`, a "valuable" session (interventions or substantial tool use, per Decision 8) is committed to the team repo at sessions/<user>/<month>.md — the path digest already reads, so "Session Highlights" lights up with no change to digest.ts. Reuses existing infrastructure rather than a new collection path: - dashboard-collector readEvents() + aggregateSessionMetrics() for the data - utils/git pushRepoDirectly() for the direct team commit (no MR), like contribute - redact() (from #191) on the only free text included (the first-prompt line) Review feedback addressed (round 1): - Placed under a `session` subcommand group (`teamai session save`) instead of a top-level verb, matching how `roles` / `source` are organized, leaving room for `session list` / `session show` later. - Team upload defaults to counts + tools only; the redacted first-prompt line is opt-in via `--include-prompt`, since redact() is best-effort. Local logs keep it. Review feedback addressed (round 2): - Fixed user-facing command strings that still said `teamai save-session` (an unknown command): the read-only error hint and the retry hint now say `teamai session save --push`; aligned docstrings in save-session.ts / session-collector.ts / types.ts and the design-doc references. - `assertNotReadOnly` on the --push path is now wrapped in try/catch: an HTTP-mode (read-only) team prints a friendly message and keeps the local log instead of throwing an uncaught stack trace. - Idempotency key is now the full session id in a non-rendering HTML comment (`<!-- teamai:session <id> -->`) rather than the 8-char short id, removing the ~1-in-4B same-month prefix collision that could silently drop a session. - Replaced the ad-hoc `Promise.race`/`setTimeout` push guard with the shared `withTimeout()` helper (src/utils/async.ts, from #211), which clears its timer in a finally so the process doesn't linger after a fast push. Team upload is opt-in; local logs are pruned after 90 days (Feature 5 retention). Test Plan: - npx tsc --noEmit - npx vitest run (145 files, 1801 tests; 15 in session-collector.test.ts, incl. a new "distinct sessions sharing an 8-char id prefix" case) - npm run build - real-CLI E2E: seeded ~/.teamai/dashboard/events.jsonl, ran `node dist/index.js session save --session-id <sid>`; wrote ~/.teamai/session-logs/2026-07.md with folded stats + full-id marker, a `ghp_` token in the prompt came out `<REDACTED:gh_tok>`, a second run was idempotent; `--push` against a read-only HTTP team printed the graceful message (exit 0, no stack trace); old `save-session` errors as unknown; `--include-prompt` appears in `session save --help`. Co-authored-by: Eyre921 <Eyre921@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a small, dependency-free secret-redaction utility (
src/utils/redact.ts) and wires it into the one place TeamAI currently persists raw assistant text — the dashboard'sstoppedOutput— so a token pasted into a conversation can no longer land in~/.teamai/dashboard/events.jsonl(or the rendered dashboard). This is the missing primitive behind the "counts only, no prompt text" posture: with deterministic scrubbing available, later features can safely persist richer session content behind explicit opt-in.Type of Change
What it does
redact(text, options)scrubs likely secrets in two layers, applied in order:collectEnvSecrets()) — 100% precise for values already in the environment. Longest values are masked first so an overlapping shorter one leaves no fragment.sk-ant-,sk-,ghp_,github_pat_, AWSAKIA…, Slack, Google, Stripe, and a provider catalog), PEM private-key blocks, JWTs,key=valuepairs,Authorization: Bearerheaders, and connection-string passwords. This catches secrets pasted into a conversation that never touched the environment.Structural patterns keep surrounding context (
password = <REDACTED:kv>,Authorization: Bearer <REDACTED:authz>,postgres://admin:<REDACTED:conn>@host). Redaction runs after any analytics extraction, so metrics are unaffected.Why vendored rather than a dependency
This runs on the hook hot path (every session event cold-starts the CLI), and the project keeps a small dependency surface and a Node 20 floor. The mature options don't fit that baseline: external scanners (gitleaks, trufflehog) are separate binaries; Node-native linters (secretlint) are dev-time, file-oriented, and require a newer runtime. Those tools use the same regex catalog inlined here, without the framework weight. A heavier external scanner is best layered later as an optional, opt-in deep pass — not the always-on floor.
Test Plan
npx tsc --noEmitpassesnpx vitest runpasses — 139 files, 1729 testsredact.test.ts; a regression test indashboard-collector.test.tsasserting aghp_…token in the last assistant message is maskednpm run buildsucceedsStopevent with a fakeghp_…token throughnode dist/index.js hook-dispatch stop --tool claude; the resultingevents.jsonlstoppedOutputreadGH_TOKEN=<REDACTED:gh_tok>and the raw token was absent.Notes for Reviewers
readLastAssistantOutput(); everything else is a pure, standalone utility with no callers yet, so blast radius is small.README.zh-CN.mdupdate is needed.