Skip to content

Commit fe4395f

Browse files
bakeyclaude
andauthored
docs(skills): source-pack lessons from the Feishu live pass and the Notion loader review (#190)
* docs(skills): source-pack lessons from the Feishu live pass and the Notion loader review Three families of hard-won guidance folded back into the skill: - live-verification.md gains an OAuth-provider section (the credential is a flow: app + redirect URI + config PUT + browser authorization) and its four live-verified traps — provider scope unions the app cannot enable (Feishu 20027 → oomol-lab/open-connector#267), gateway-declared scopes the API does not honor (99991679 named im:message:readonly against declared get_as_user scopes → #268), capability/admin gates beyond scopes entirely (bot ability 232025, sensitive-permission approval), and grant-time token snapshots (re-authorize after every scope change). - Probing now covers declared INPUT BOUNDS (Feishu's declared page_size max 100 vs the wire's 50, 99992402 → #269/#271) and the termination signal ON THE REAL FINAL PAGE (Feishu wiki's non-empty token beside has_more:false → has_more_path, #270). - review-checklist.md hardens two items from the Notion approval review: the failure arm of every gate must be reachable through the PUBLIC entry point (serde's untagged buffering destroyed the evidence first_non_finite was checking for — dead code by construction), and every wire e2e asserts the EXACT input key set per request, absence included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skills): source-pack redaction and doc-sync lessons from the Feishu round-2 review The round-2 pass on the Feishu pack (PR #186) found a P1 the skill's existing rules were one level too shallow to prevent, plus four process gaps. Folded back: - Redaction must DECODE nested JSON-encoded strings and audit their leaves with the same allowlist (real member names survived inside body.content while the outer-tree audit passed), and the audit ships as an in-repo tripwire test so CI enforces the guarantee. - Person-linked capture timestamps are coarsened; redacted cross-references must stay self-consistent (a row's url embedding its own guid); PII that ever reached a commit means branch-history rewrite, not a tip edit. - Wire pins assert declared constants by VALUE (every table's page_size number), not key presence — pageSize is where a live contract defect actually surfaced. - Zero-fixture-evidence columns are annotated doc-derived at the declaration. - The pack doc's pushdown matrix is re-derived from the FINAL yaml after the live pass (a dropped pushdown must read `—`), and upstream gateway defects get filed as issues and linked from the pack doc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 2291b19 commit fe4395f

3 files changed

Lines changed: 131 additions & 6 deletions

File tree

docs/superpowers/skills/source-pack/SKILL.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,15 @@ missing invariant (with regression tests) is then prerequisite work.
128128
Implementation done and tests green is NOT done. Follow
129129
[references/live-verification.md](references/live-verification.md):
130130
have the user configure a real (free-tier) provider account in the
131-
gateway — you never touch the credential — then probe every action with
132-
the pack's exact inputs, diff real row keys against the mapped columns
133-
in both directions, scan every table end to end through skardi-server
131+
gateway — you never touch the credential; for OAuth providers the
132+
reference maps the full flow and its layered gates (scope unions the
133+
app cannot enable, gateway-declared scopes the API does not honor,
134+
capability flags and admin approvals beyond scopes, stale-token
135+
snapshots) — then probe every action with
136+
the pack's exact inputs AND the declared input bounds, diff real row
137+
keys against the mapped columns
138+
in both directions, verify termination on the real final page, scan
139+
every table end to end through skardi-server
134140
(registration passes the fingerprint gate against LIVE discovery; every
135141
mapped column extracts a real non-NULL value somewhere; pinned filters
136142
actually return rows; a small page size forces real multi-page

docs/superpowers/skills/source-pack/references/live-verification.md

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,41 @@ accept it explicitly.
5555
the tables (e.g. for Notion: share a page and a database with the
5656
integration).
5757

58+
### OAuth providers: the credential is a flow, and scopes are not the only gate
59+
60+
For `authTypes: ["oauth2"]` providers the setup is heavier than an API
61+
key, and the Feishu pack's live pass (skardi PR #186) hit every trap in
62+
sequence — expect them:
63+
64+
- **The flow**: the user creates the provider app themselves and
65+
registers the gateway's redirect URI (`http://localhost:3000/oauth/callback`);
66+
the user runs `PUT /api/oauth/configs/<service>` with
67+
`{clientId, clientSecret}` from their env; then
68+
`POST /api/oauth/authorizations {"service": "<service>"}` returns an
69+
`authorizationUrl` the USER opens in a browser. You never touch the
70+
secret or the browser session.
71+
- **The gateway may request an un-satisfiable scope set.** Providers
72+
that build their OAuth scope list as the union of every action's
73+
permissions can exceed what any real app enables (Feishu rejects the
74+
authorize request outright, error 20027). Narrowing may require a
75+
clearly-marked local patch to the provider definition plus an
76+
upstream issue — precedent: oomol-lab/open-connector#267.
77+
- **Scopes granted ≠ scopes the API accepts.** A read can 401 with the
78+
gateway-declared scopes present in the token: the provider error
79+
often names the ACTUAL required scope, and the gateway's
80+
`requiredScopes` metadata is then the bug (Feishu's 99991679 named
81+
`im:message:readonly` while the actions declared `*.get_as_user`
82+
oomol-lab/open-connector#268). Read the provider's own error before
83+
suspecting your pack.
84+
- **Capability gates sit beyond scopes entirely**: app-level abilities
85+
(Feishu's bot capability, 232025) and tenant-admin approval of
86+
high-sensitivity permissions are enforced independently of the OAuth
87+
grant. Budget for console round-trips with the user.
88+
- **Every scope/permission change needs a FRESH token** — a
89+
user_access_token snapshots its grants at authorization time, so
90+
re-run the authorization after any change rather than debugging a
91+
stale token.
92+
5893
## 1. Probe every action with real inputs first
5994

6095
Before involving Skardi, call each chosen action directly
@@ -70,7 +105,20 @@ scratch directory (they become the fixture sources in §4). Check:
70105
- the real pagination envelope: force multi-page with a small page size
71106
(`pageSize: 1`) and confirm the continuation token appears where the
72107
pack's `next_cursor_path` / totals path expects it, and terminates on
73-
the documented spelling.
108+
the documented spelling;
109+
- **the declared input bounds, at the boundary**: send the pack's exact
110+
page size and the schema's declared maximum — declared caps can
111+
exceed the wire's (Feishu's `im/v1/messages` declares 100, hard-fails
112+
above 50 with 99992402; skardi PR #186 shipped the corrected 50, and
113+
oomol-lab/open-connector#269/#271 fixed the schema upstream);
114+
- **the termination signal on the REAL final page**: fetch to the end,
115+
then deliberately follow whatever token the last page returns. Some
116+
providers answer the final page with `has_more: false` beside a
117+
NON-empty token (Feishu wiki's `"0||…"`), so null-token termination
118+
refetches a finished scan and trips the loop guard — declare
119+
`has_more_path` when the envelope carries an authoritative has-more
120+
boolean, and pin the live shape with an e2e
121+
(oomol-lab/open-connector#270 tracks the upstream normalization).
74122

75123
## 2. Diff real rows against the mapped columns — both directions
76124

@@ -140,9 +188,36 @@ timestamp spellings, real URL shapes. Redaction methodology:
140188
timestamps, known structural enums). Anything that survives the
141189
filter gets looked at by eye. Real page titles hiding inside URL
142190
slugs are exactly what this catches.
191+
- **decode one level deeper**: any string value that itself parses as
192+
JSON (Feishu's `body.content`, Slack blocks) must be decoded and its
193+
string leaves run through the SAME allowlist. The Feishu round-2
194+
blocker was exactly this: real member names survived inside a
195+
JSON-encoded payload the outer-tree audit treated as one opaque
196+
string.
197+
- **ship the audit as a tripwire test**, not a one-off pass: an
198+
in-repo test that re-walks every fixture (including the nested
199+
decode) so the redaction guarantee is enforced by CI, not by memory.
200+
A cheap broad net helps too — e.g. "no CJK text in any fixture" when
201+
the workspace's real names share a script no placeholder uses.
202+
- **coarsen capture timestamps** when rows encode person-linked events
203+
(joins, messages, task completions): zero the trailing digits so the
204+
instant stops being correlatable while magnitude and ordering (and
205+
any digit-count-sensitive parsing) survive. Update tests that pinned
206+
exact values.
207+
- **verify redaction self-consistency**: the deterministic counter only
208+
helps if cross-references actually still line up — after redacting,
209+
check that ids repeated across fields (a task's `url` embedding its
210+
own `guid`) still match, and that provider-unique ids stayed unique.
211+
A fixture whose value is "internally consistent live capture" must
212+
survive its own cross-references.
143213
- deliberately-broken fixtures (the admission gate's schema-mismatch
144214
case) stay synthetic — say so in a comment.
145215

216+
**If PII ever lands in a commit**: fixing the tip is not enough — the
217+
names remain in every earlier commit. Rewrite the branch history
218+
(squash/amend and force-push) so no reachable commit carries them, and
219+
say so in the review reply.
220+
146221
Then update the fixture-driven tests to assert the live shapes, and the
147222
coverage-gap pins if columns moved.
148223

@@ -152,7 +227,9 @@ The PR must let a reviewer see the verification without re-running it:
152227
per-table live results (row counts, which pinned filters returned rows,
153228
which columns carried real values), the pinned provider API version,
154229
what the live pass CHANGED (renamed/dropped/added columns and why), and
155-
what remains outside the fingerprint gate. Put the durable facts in the
230+
what remains outside the fingerprint gate. Upstream gateway defects the
231+
pass uncovered get FILED as issues on the gateway repo and linked from
232+
the pack doc — findings that live only in a PR body get lost. Put the durable facts in the
156233
module doc and pack doc; put the run evidence in the PR description or
157234
a comment. Stop the gateway and skardi-server when done, and remind the
158235
user to rotate any credential that was exposed.

docs/superpowers/skills/source-pack/references/review-checklist.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ The worst failure class: wrong results with a green status.
2525
authoritative total) — a filtered count is never a termination
2626
signal, and a missing signal means upstream contribution or
2727
deferral, not a heuristic.
28+
- [ ] Termination verified on the REAL final page, not assumed from the
29+
envelope shape: providers can return a non-empty continuation
30+
token beside `has_more: false` (Feishu wiki), which null-token
31+
termination refetches until the loop guard kills the scan.
32+
Declare `has_more_path` where the envelope carries an
33+
authoritative has-more boolean, and pin the live final-page shape
34+
with an e2e.
2835
- [ ] Short/empty non-final pages cannot truncate: if the envelope has
2936
an authoritative total, the strategy declares `total_pages_path`;
3037
if not, the heuristic's limits are documented.
@@ -77,9 +84,26 @@ The worst failure class: wrong results with a green status.
7784
- [ ] Per-declaration coverage: every table's own wire declarations
7885
(row path, input keys, pagination params) pinned by its own e2e —
7986
shared constants are not shared coverage.
87+
- [ ] Every wire e2e asserts the EXACT input key set per request
88+
(sorted keys equal, absence included — page 1 carries no cursor),
89+
not key presence: presence-only assertions cannot catch an
90+
undeclared extra leaking onto the wire, and strict action schemas
91+
turn that extra into a runtime 400.
92+
- [ ] Declared constants asserted by VALUE, not key presence — every
93+
table's `page_size` pinned to its number on the wire (`pageSize`
94+
is exactly where a live contract defect surfaced: declared 100,
95+
wire caps at 50).
8096
- [ ] Both sides of every gate: the pass path (suite-wide) and the fail
8197
path (targeted test). A gate whose failure arm no test exercises
82-
is dead code until proven otherwise.
98+
is dead code until proven otherwise — and the failing input must
99+
reach the gate THROUGH THE PUBLIC ENTRY POINT, not by calling the
100+
guard function directly. Deserialization layers can destroy the
101+
evidence before a post-hoc guard runs: serde_json's f64 visitor
102+
converts a nested `.nan` to `null` during untagged buffering, so a
103+
"reject non-finite" walk over the deserialized value could never
104+
fire (the `first_non_finite` finding on the Notion PR — the fix
105+
captures `serde_yaml::Value` and converts fallibly where the
106+
evidence still exists).
83107
- [ ] Error tests assert the full identity (column/path/page/row/
84108
expected/found-kind) and that the offending VALUE never appears.
85109
- [ ] Negative-space guards for every "this deliberately doesn't
@@ -101,6 +125,18 @@ The worst failure class: wrong results with a green status.
101125
audited mechanically (every surviving string matched against an
102126
allowlist — real titles hide in URL slugs). Deliberately-broken
103127
fixtures (schema-mismatch) stay synthetic and say so.
128+
- [ ] The redaction audit DECODES nested JSON-encoded strings and
129+
audits their leaves too (real names survived one decode level
130+
down in a message payload), and it ships as an in-repo tripwire
131+
test so CI enforces it. Person-linked capture timestamps are
132+
coarsened; redacted cross-references stay self-consistent (an
133+
id embedded in the row's own URL matches the row). If PII ever
134+
reached a commit, the branch history was rewritten, not just the
135+
tip.
136+
- [ ] Columns with ZERO fixture evidence (no captured row carries the
137+
key) are annotated doc-derived at the declaration — under a
138+
loose-schema pack, real rows are the only column truth, so an
139+
evidence gap must be a reviewed fact, not an implicit one.
104140
- [ ] No real orgs/users/tokens anywhere; if a credential was ever
105141
pasted into a conversation or log during verification, the user
106142
was told to rotate it.
@@ -112,6 +148,12 @@ The worst failure class: wrong results with a green status.
112148
- [ ] No stale references: milestone numbers in Review notes, removed
113149
columns/tests still described, fixture-category lists, "pending"
114150
markers for work that has since landed.
151+
- [ ] The pack doc's table/pushdown matrix re-derived from the FINAL
152+
yaml after the live pass — a pushdown the reconciliation dropped
153+
must read ``, not survive as a promise (the doc row is the
154+
easiest artifact to forget when the wire invalidates a draft).
155+
- [ ] Upstream gateway defects found during verification are filed as
156+
issues on the gateway repo and LINKED from the pack doc.
115157
- [ ] Operational consequences documented where behavior surprises:
116158
fingerprint pins fail on ANY schema change (additive included —
117159
re-capture and re-pin on upstream upgrades); raw-scan default-deny

0 commit comments

Comments
 (0)