mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
---
|
||||
issue: 307
|
||||
issue_title: "Project a running effective working directory across cd's onto bash path candidates"
|
||||
---
|
||||
|
||||
# Project a running effective working directory onto bash path candidates
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The bash external-directory guard resolves every relative path candidate in a command against a single base.
|
||||
`BashProgram.parse()` records one `leadingCdTarget` — the argument of the very first command, and only when that first command is `cd` — and `externalPaths(cwd)` resolves all candidates against `cwd` joined with that one target.
|
||||
|
||||
This single-base model is wrong in both directions.
|
||||
It over-prompts on benign multi-`cd` paths, and — worse — it can miss a real escape.
|
||||
For `cd nested/deep && cd .. && cat ../../etc/passwd` the real effective directory is `cwd/nested`, so `../../etc/passwd` escapes to `cwd/../etc/passwd` and should be flagged; today we resolve against `cwd/nested/deep` (the first `cd` only), which collapses back inside `cwd` and sails through.
|
||||
|
||||
The fix is to project the effective working directory at each point in the command stream onto that point's path candidates.
|
||||
The effective directory is stateful: it starts at `cwd`, each current-shell `cd <literal>` mutates it for subsequent commands, and subshell / brace-group / pipeline / background contexts scope changes that must not leak.
|
||||
A relative candidate must resolve against the directory in force *where it appears*, not against one base for the whole string.
|
||||
|
||||
## Goals
|
||||
|
||||
- Retire the single `leadingCdTarget: string | undefined` in favor of a per-candidate effective base.
|
||||
- Tier 1 — fold a sequence of top-level current-shell `cd <literal>` commands (joined by `&&`, `||`, `;`, or a newline) into a running effective directory, and resolve each candidate against the directory in force where it appears.
|
||||
- Tier 2 — model subshell `( … )` and command/process-substitution scoping with a directory-frame stack (a `cd` inside resets on exit), persist a `cd` inside a `{ … }` brace group (brace groups run in the current shell), and ensure `cd`s inside pipelines (`a | b`) and backgrounded commands (`a &`) never update the running directory.
|
||||
- Conservative bail — when a `cd` target is not a static literal (`cd "$DIR"`, `cd $(…)`, `cd -`), the effective directory becomes unknown; subsequent **relative** candidates are treated as potentially external and flagged (least-privilege).
|
||||
- Preserve `pathTokens()` exactly — it is cwd-independent and must not change.
|
||||
- This change makes external-directory decisions more precise; it is a behavior change (more escapes caught, occasional new prompts), surfaced via `feat:` commits.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Variable- or substitution-valued `cd` targets, `pushd` / `popd`, `cd -` / `$OLDPWD`, `CDPATH` — these only mark the effective directory unknown (the conservative bail), they are not resolved.
|
||||
- Modeling `&&` / `||` success or failure — assume each `cd` may take effect.
|
||||
- Symlink resolution, physical vs. logical paths (`cd -P`).
|
||||
- `eval`, sourced scripts, functions, aliases.
|
||||
- Folding `cd` state inside control-flow bodies (`if` / `while` / `for` / `case`) and function definitions — their candidates are collected against the base in force at entry, but their internal `cd`s do not fold (deferred, conservative).
|
||||
- Changing the command-pattern slice (`commands()` / `resolveBashCommandCheck`) or the `BashCommand` model — this issue touches only the path-candidate slice.
|
||||
- Changing `pathTokens()` output or the two extractor facades' signatures.
|
||||
|
||||
## Background
|
||||
|
||||
Relevant modules (all changes are private to `bash-program.ts` plus its tests):
|
||||
|
||||
- `src/handlers/gates/bash-program.ts` — `BashProgram.parse(command)` walks the AST once, today producing `rawTokens: string[]`, `leadingCdTarget: string | undefined`, and `commandUnits: BashCommand[]`.
|
||||
`externalPaths(cwd)` resolves every `rawTokens` candidate against `computeEffectiveResolveBase(leadingCdTarget, cwd)`; `pathTokens()` rule-classifies the same `rawTokens` (cwd-independent).
|
||||
`extractLeadingCdTarget` (via `findFirstCommand`, which descends only `program` / `list` and takes the first command) and `computeEffectiveResolveBase` are the two private helpers this issue retires.
|
||||
- `src/handlers/gates/bash-token-classification.ts` — `classifyTokenAsPathCandidate` (strict: accepts a token only if it starts with `/`, starts with `~/`, or contains `..`) and `classifyTokenAsRuleCandidate` (broader).
|
||||
This is the load-bearing fact: the strict external-directory classifier never accepts a bare relative filename (`x`, `src/foo.ts`), so the effective base only ever changes the resolution of `..`-containing relative candidates.
|
||||
- `src/handlers/gates/bash-path-extractor.ts` — `extractExternalPathsFromBashCommand(command, cwd)` / `extractTokensForPathRules(command)` facades over `BashProgram`; signatures unchanged.
|
||||
- `src/handlers/gates/bash-external-directory.ts` / `bash-path.ts` — the two gates; they call `bashProgram.externalPaths(cwd)` / `.pathTokens()`.
|
||||
Neither gate signature changes.
|
||||
- `src/path-utils.ts` — `normalizePathForComparison(candidate, base)` = `resolve(base, candidate)` + normalize; `isPathWithinDirectory`, `isSafeSystemPath` unchanged.
|
||||
|
||||
Constraints from `AGENTS.md` / the package skill that apply:
|
||||
|
||||
- Default to least privilege; silent over-matching is a permission bypass — the conservative-bail decision (flag unknown-base relatives) follows this.
|
||||
- Do not read `process.cwd()` inside the walk — `cwd` is already a parameter of `externalPaths(cwd)`, and the parse-time walk computes only a relative offset, never resolving against `cwd`.
|
||||
- New / changed `BashProgram` methods keep the `// fallow-ignore-next-line unused-class-member` suppression (private-ctor false positive).
|
||||
- Run `pnpm run check` immediately after each interface-changing step; run the full suite after any step that touches the shared walk.
|
||||
|
||||
AST facts (verified during planning with disposable `web-tree-sitter` probes):
|
||||
|
||||
| Input | Tree (relevant shape) |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `cd a && cd b && cat x` | `program > list > [list > [command, &&, command], &&, command]` (left-associative) |
|
||||
| `mkdir d && cd d && cat x` | same `list` nesting; `cd` is the second command |
|
||||
| `cd a & cat x` | `program > [command, ·&, command]` — `&` is an anonymous token *after* the backgrounded command |
|
||||
| `cat a \| cd b` | `program > pipeline > [command, \|, command]` |
|
||||
| `( cd sub && cat y ) && cat z` | `program > list > [subshell > list, &&, command]` |
|
||||
| `cd a && { cd b; cat c; }` | `program > list > [command, &&, compound_statement > [command, command]]` |
|
||||
| `echo $(cd q && cat r)` | `command > command_name > [echo, command_substitution > list]` |
|
||||
| `cat $(cd s; pwd) y` | `command > [command_name, command_substitution, word "y"]` — `y` is an outer-scope sibling |
|
||||
|
||||
`&` (background) is distinguished from `&&` / `||` / `;` by the anonymous operator token type that follows a command — the cd-fold honors `&&` / `||` / `;` / newline and skips `&`.
|
||||
|
||||
## Design Overview
|
||||
|
||||
### What actually changes
|
||||
|
||||
Because the strict classifier only admits absolute, `~/`, and `..`-relative tokens, and absolute / `~/` tokens are independent of the working directory, the effective base affects exactly one class of candidate: **`..`-containing relative paths**.
|
||||
The entire feature is "resolve `..`-relative external-directory candidates against the effective directory in force where they appear, and flag them conservatively when that directory is unknown." `pathTokens()` never resolves against a base, so it is provably unaffected.
|
||||
|
||||
### Per-candidate effective base
|
||||
|
||||
`rawTokens: readonly string[]` and `leadingCdTarget: string | undefined` are replaced by a single list of candidates carrying their projected base:
|
||||
|
||||
```typescript
|
||||
// src/handlers/gates/bash-program.ts
|
||||
|
||||
/**
|
||||
* The working directory in force where a path candidate appears, expressed as
|
||||
* an offset to be joined with `cwd` at resolution time (the walk never sees
|
||||
* `cwd`). `known` carries a relative-or-absolute offset string built by folding
|
||||
* `cd` literals ("" = cwd); `unknown` marks a non-literal `cd` that made the
|
||||
* effective directory unresolvable.
|
||||
*/
|
||||
type EffectiveBase =
|
||||
| { readonly kind: "known"; readonly offset: string }
|
||||
| { readonly kind: "unknown" };
|
||||
|
||||
interface PathCandidate {
|
||||
readonly token: string;
|
||||
readonly base: EffectiveBase;
|
||||
}
|
||||
```
|
||||
|
||||
The constructor stores `rawCandidates: readonly PathCandidate[]` in place of `rawTokens` + `leadingCdTarget`; `commandUnits` is unchanged.
|
||||
|
||||
### Slices derive from the one list
|
||||
|
||||
```typescript
|
||||
pathTokens(): string[] {
|
||||
// Identical to today: rule-classify + dedup over the candidate tokens,
|
||||
// ignoring base. Source order and token set are unchanged.
|
||||
}
|
||||
|
||||
externalPaths(cwd: string): string[] {
|
||||
const normalizedCwd = normalizePathForComparison(cwd, cwd);
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const { token, base } of this.rawCandidates) {
|
||||
const candidate = classifyTokenAsPathCandidate(token);
|
||||
if (!candidate) continue;
|
||||
|
||||
// Unknown base + relative candidate → conservative: always external.
|
||||
if (base.kind === "unknown" && isRelativeCandidate(candidate)) {
|
||||
pushExternalForDisplay(candidate, cwd, seen, out); // resolve vs cwd for display, never suppress
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolveBase = base.kind === "known" ? resolve(cwd, base.offset) : cwd;
|
||||
const normalized = normalizePathForComparison(candidate, resolveBase);
|
||||
if (!normalized) continue;
|
||||
if (
|
||||
normalizedCwd !== "" &&
|
||||
!isSafeSystemPath(normalized) &&
|
||||
!isPathWithinDirectory(normalized, normalizedCwd) &&
|
||||
!seen.has(normalized)
|
||||
) {
|
||||
seen.add(normalized);
|
||||
out.push(normalized);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
`isRelativeCandidate(c)` = `!c.startsWith("/") && !c.startsWith("~")` (absolute and `~/` are base-independent and resolve normally even under an unknown base).
|
||||
`resolve(cwd, base.offset)` handles both a relative offset (`"nested"` → `cwd/nested`) and an absolute offset (`"/abs"` → `/abs`, ignoring `cwd`).
|
||||
The old `computeEffectiveResolveBase` escape-to-`cwd` fallback is dropped: a `cd` that escapes `cwd` is now tracked faithfully, so candidates after it resolve against the (external) effective directory and are flagged — which is the desired least-privilege behavior and the whole point of the missed-escape fix.
|
||||
|
||||
### The base-threading walk
|
||||
|
||||
`collectPathCandidateTokens(node): string[]` is replaced by `collectPathCandidates(rootNode): PathCandidate[]`, a single recursive walk that threads an `EffectiveBase` and emits each candidate tagged with the base in force.
|
||||
The leaf token collectors (`collectCommandTokens`, `collectGenericCommandTokens`, `collectPatternCommandTokens`, `collectRedirectTokens`) keep their existing token-selection logic so the candidate **set and order are unchanged** — they are wrapped to (a) tag direct-argument tokens with the current base and (b) hand nested substitution / subshell subtrees back to the scoped walk rather than flat-collecting them.
|
||||
|
||||
Threading rules by node type:
|
||||
|
||||
- `program` / `list` / `compound_statement` (current-shell sequences) — fold left: the offset flows through children in source order; a child's returned offset becomes the next child's input.
|
||||
A brace group `{ … }` therefore persists its internal `cd`s to following siblings (it runs in the current shell).
|
||||
- `command` — emit the command's direct path candidates tagged with the current base; recurse into nested command/process substitutions with a *scoped copy* of the base (interior `cd`s do not leak out, but interior tokens inherit and may fold within the substitution).
|
||||
If the command is `cd <literal>` at a current-shell position, return the folded offset (`join(offset, literal)`, or the literal if absolute); if it is `cd` with a non-literal target, return `{ kind: "unknown" }`; otherwise return the input base unchanged.
|
||||
A command's own `cd`-argument candidate is emitted with the *pre-update* base (the directory the `cd` runs from).
|
||||
- `redirected_statement` — emit the redirect-target candidates with the current base; fold the inner command and return its offset.
|
||||
- `pipeline` — every member runs in a subshell: walk each member with a scoped copy of the base (collecting its candidates) and discard the returned offset; return the input base unchanged.
|
||||
- backgrounded command (a statement followed by the anonymous `&` token) — walk it scoped and discard its offset; return the input base unchanged.
|
||||
- `subshell` `( … )` — push a frame: walk the interior starting from the current base, discard the returned offset (pop); return the input base unchanged.
|
||||
- command/process substitution `$(…)` / `` `…` `` / `<(…)` / `>(…)` — scoped like a subshell; interior tokens inherit the enclosing base and may fold within, but the offset does not leak to the enclosing command.
|
||||
- control-flow bodies and function definitions — collect interior candidates against the entry base, do not fold their internal `cd`s (conservative, deferred).
|
||||
|
||||
`isNamed` / `SKIP_SUBTREE_TYPES` guards are preserved exactly so heredoc bodies, comments, and operator tokens are skipped as today.
|
||||
|
||||
### Worked examples
|
||||
|
||||
- `cd nested/deep && cd .. && cat ../../etc/passwd` (cwd = `/p`): seg1 offset `nested/deep` (`nested/deep` is not a `..`/abs candidate, not flagged); seg2 candidate `..` resolves vs `/p/nested/deep` → `/p/nested` (inside, not flagged), offset folds to `nested`; seg3 candidate `../../etc/passwd` resolves vs `/p/nested` → `/p/../etc/passwd` → **outside `/p` → flagged**.
|
||||
The escape is now caught.
|
||||
- `mkdir d && cd d && cat ../x`: `cd d` is the second command but current-shell, so it folds; `../x` resolves vs `cwd/d` → `cwd/x` inside → not flagged.
|
||||
- `( cd sub && cat ../x ) && cat ../y`: inside the subshell `../x` resolves vs `cwd/sub` → `cwd/x` inside (not flagged); the subshell `cd` is popped, so `../y` resolves vs `cwd` → `cwd/../y` → flagged.
|
||||
- `cd a & cat ../x`: `cd a` is backgrounded → does not fold; `../x` resolves vs `cwd` → flagged.
|
||||
- `cd "$DIR" && cat ../x`: non-literal `cd` → base unknown; `../x` is relative → **flagged conservatively**; a sibling `cat /etc/hosts` (absolute) still resolves normally and is flagged on its own merits.
|
||||
|
||||
### Design-review notes
|
||||
|
||||
- Dependency width: `rawCandidates` replaces two fields (`rawTokens` + `leadingCdTarget`) with one; `externalPaths` / `pathTokens` each consume it fully.
|
||||
No gate signature changes, so no dependency-bag widening crosses a module boundary.
|
||||
- Law of Demeter: gates still call `bashProgram.externalPaths(cwd)` / `.pathTokens()` — methods on the injected collaborator.
|
||||
- The `BashCommand` model is deliberately **not** extended with path candidates, despite the forward note in the [#308] plan.
|
||||
The cwd-frame grouping needed here descends into brace groups and substitution interiors and folds `cd` state, whereas `commands()` emits brace groups whole and emits nested commands as separate rule units — different descent semantics.
|
||||
Conflating them would force a discriminator and leak one slice's descent policy into the other; keeping the path-candidate walk as its own derivation of the shared single parse honors the [#308] anti-drift goal (one parse, two faithful slices) without the wrong abstraction.
|
||||
|
||||
## Module-Level Changes
|
||||
|
||||
1. `src/handlers/gates/bash-program.ts`:
|
||||
- Add the `EffectiveBase` type and `PathCandidate` interface (module-private).
|
||||
- Constructor: replace `rawTokens` + `leadingCdTarget` parameters with `rawCandidates: readonly PathCandidate[]`.
|
||||
- `parse()`: call the new `collectPathCandidates(rootNode)`; remove the `extractLeadingCdTarget(...)` call.
|
||||
- `pathTokens()`: iterate `rawCandidates`, rule-classify `token`, dedup (behavior identical).
|
||||
- `externalPaths(cwd)`: resolve per-candidate base as above; add `isRelativeCandidate` + the unknown-base conservative branch; remove the `computeEffectiveResolveBase` call.
|
||||
- Replace `collectPathCandidateTokens` with the base-threading `collectPathCandidates` walk and its scope-aware helpers; wrap the existing leaf collectors to tag tokens and delegate nested subtrees.
|
||||
- Remove `findFirstCommand`, `extractLeadingCdTarget`, and `computeEffectiveResolveBase` (sole callers removed in this file).
|
||||
- Keep the `// fallow-ignore-next-line unused-class-member` suppressions on `pathTokens` / `externalPaths`.
|
||||
2. `test/handlers/gates/bash-program.test.ts` — extend the `externalPaths` describe with the new cases (multi-`cd` fold, `cd`-not-first, missed-escape, subshell scoping + non-leak, brace-group persistence, pipeline / background non-leak, unknown-base conservative + absolute-still-normal).
|
||||
3. `test/bash-external-directory.test.ts` — in the `leading cd prefix` describe, re-frame the two tests whose comments encode the retired single-`cd`/fallback model and strengthen their assertions to the new resolved paths:
|
||||
- `"cd is not first command: cd is ignored"` → the second `cd` now folds; rename to reflect sequential-fold semantics and assert the resolved escape path.
|
||||
- `"cd to external dir: paths after cd are still checked against cwd"` → faithful tracking now resolves against `/tmp`; update the comment and assert the new resolved path (`/etc/hosts`) plus the `/tmp` candidate.
|
||||
The single-leading-`cd` regression tests stay green unchanged (a single leading `cd` is the one-element case of the fold).
|
||||
4. `docs/architecture/architecture.md` — update the `bash-program.ts` listing line: `externalPaths(cwd)` projects a running effective working directory across a sequence of current-shell `cd`s with subshell / brace-group / pipeline / background scoping and a conservative unknown-base bail (retiring the single `leadingCdTarget`); note that `pathTokens()` is unchanged.
|
||||
|
||||
No `README.md`, `docs/configuration.md`, schema, or example-config changes — no config surface changes.
|
||||
`docs/architecture/v3-architecture.md` is historical narrative, left unchanged.
|
||||
|
||||
## Test Impact Analysis
|
||||
|
||||
1. New unit coverage enabled — the per-candidate base makes each Tier directly testable at the `BashProgram.externalPaths` boundary: sequential fold, `cd`-not-first fold, the missed-escape regression, each Tier-2 scope (subshell frame, brace-group persistence, pipeline / background non-leak), and the unknown-base conservative branch (relative flagged, absolute normal).
|
||||
These were previously impossible because the single `leadingCdTarget` collapsed all of them to one base.
|
||||
2. Tests that become redundant — none are deleted.
|
||||
Two `leading cd prefix` tests are re-framed (their loose `length > 0` assertions stay green by coincidence, but their comments/titles asserted the retired model); strengthening them to exact resolved paths turns a coincidental pass into documentation of the new behavior.
|
||||
3. Tests that must stay as-is — the single-leading-`cd` regression and within-cwd cases in `bash-external-directory.test.ts`, the entire `extractTokensForPathRules` / `pathTokens` block (cwd-independent, must not move), and the `commands()` / gate / tool-call suites (this issue does not touch the command-pattern slice).
|
||||
The 1027-line characterization suite is the primary guard that `pathTokens` and the unchanged-base candidate set did not drift.
|
||||
|
||||
## TDD Order
|
||||
|
||||
1. `feat: fold sequential current-shell cd into the bash effective directory` — introduce `EffectiveBase` / `PathCandidate`, replace `rawTokens` + `leadingCdTarget` with `rawCandidates`, build the base-threading `collectPathCandidates` walk handling current-shell `cd` folding over `&&` / `||` / `;` / newline while **excluding** backgrounded (`&`) and pipeline-member `cd`s, and resolve per-base in `externalPaths`; subshell / brace-group / substitution interiors inherit the outer base without folding their internal `cd`s (correct Tier-1 conservative behavior).
|
||||
Retire `findFirstCommand` / `extractLeadingCdTarget` / `computeEffectiveResolveBase`.
|
||||
Add the multi-`cd`, `cd`-not-first, missed-escape, and pipeline / background non-leak tests; re-frame the two `bash-external-directory.test.ts` tests.
|
||||
This is one atomic step: the `rawTokens` → `rawCandidates` representation change breaks both `pathTokens` and `externalPaths` internally at once.
|
||||
Run `pnpm run check`, then the full suite.
|
||||
2. `feat: scope cd inside subshells and persist it across brace groups` — extend the walk so subshell `( … )` and command/process-substitution interiors fold their internal `cd`s within a popped frame, and brace groups `{ … }` persist their `cd`s to following current-shell siblings.
|
||||
Add the subshell-internal fold, subshell non-leak, brace-group persistence, and substitution-internal scoping tests.
|
||||
Run `pnpm run check`, then the full suite.
|
||||
3. `feat: flag relative paths conservatively after a non-literal cd` — mark a non-literal `cd` target (`cd "$DIR"`, `cd $(…)`, `cd -`) as `{ kind: "unknown" }`, propagate it through subsequent current-shell commands, and in `externalPaths` flag relative candidates under an unknown base unconditionally while resolving absolute / `~/` candidates normally.
|
||||
Add the unknown-base conservative tests (relative flagged, absolute still normal, `cd -` unknown).
|
||||
Run `pnpm run check`, then the full suite.
|
||||
4. `docs: document effective-cwd projection in the bash gate architecture` — update the `bash-program.ts` architecture listing line.
|
||||
|
||||
After step 3, a bash command's external-directory candidates each resolve against the effective directory in force where they appear, with a conservative bail on unknowable directories.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
1. The base-threading walk drifts the candidate set, changing `pathTokens` or an unchanged-base `externalPaths` result — Mitigation: the leaf collectors keep their exact token-selection logic; the 1027-line characterization suite plus the `pathTokens` unit cases assert the set and order are unchanged.
|
||||
Run the full suite after steps 1–3.
|
||||
2. Left-associative `list` nesting is mis-ordered, folding `cd`s out of source order — Mitigation: the walk recurses `list` children in order and threads the offset through nested `list` nodes; the AST nesting was probed during planning and the multi-`cd` test asserts source-order fold.
|
||||
3. Background `&` is mis-detected as a current-shell separator, leaking a backgrounded `cd` — Mitigation: the fold inspects the anonymous operator token following each command and folds only on `&&` / `||` / `;` / newline; a dedicated `cd a & cat ../x` non-leak test guards it.
|
||||
4. The dropped escape-to-`cwd` fallback changes a previously-green assertion — Mitigation: the only affected tests are the two re-framed `leading cd prefix` cases; both are updated in step 1 with strengthened assertions, and the change is the intended faithful-tracking fix.
|
||||
5. Conservative unknown-base over-prompts on common benign relatives — Mitigation: the strict classifier already excludes bare relative filenames, so only `..`-containing relatives under an unknown `cd` are flagged — a narrow, genuinely-ambiguous set; the decision was confirmed with the owner (least-privilege).
|
||||
6. fallow flags `pathTokens` / `externalPaths` as unused class members (private-ctor false positive) — Mitigation: carry the existing `// fallow-ignore-next-line unused-class-member` suppressions; run `pnpm fallow dead-code` from the repo root before committing.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether the substitution-internal `cd` fold (Tier 2) is worth its small added walk complexity given how rare `echo $(cd q && cat ../r)` is in practice.
|
||||
Planned in because it falls out of the same recursive frame mechanism that subshells need; if it proves to add disproportionate branching during implementation, it can be reduced to "substitution interiors inherit the outer base without internal folding" (still conservative) without affecting Tiers 1 or the unknown-base bail.
|
||||
|
||||
[#308]: https://github.com/gotgenes/pi-packages/issues/308
|
||||
Reference in New Issue
Block a user