@zalom/plastic 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLASTIC-reference.md +1 -0
- package/PLASTIC.md +5 -0
- package/package.json +1 -1
- package/scripts/feedback-report +54 -0
- package/scripts/lib/feedback_report.rb +168 -0
- package/scripts/lib/installer_core.rb +2 -0
- package/skills/feedback/SKILL.md +98 -0
- package/skills/feedback/references/transport-and-privacy.md +65 -0
- package/skills/feedback/report.md +36 -0
package/PLASTIC-reference.md
CHANGED
|
@@ -127,6 +127,7 @@ Detailed conventions live inside the skills that use them, not in this file.
|
|
|
127
127
|
| Index maintenance | `plastic-store-indexing` | — |
|
|
128
128
|
| Releases, deprecations | `plastic-releasing` | deprecation process |
|
|
129
129
|
| Health diagnostics | `plastic-doctor` | three scopes: `--core` (binary install-integrity check, runs on SessionStart), `--store [global\|<slug>]` (per-store check, runs on dashboard load), no flag = full check (runs after every update); gate enforcement, stuck detection |
|
|
130
|
+
| Report a Plastic quirk, bug, or feature idea | `plastic-feedback` | transport and privacy (redaction checklist, why a prefilled URL) |
|
|
130
131
|
| Authoring skills, agents, hooks | `plastic-skill-creating` | progressive disclosure, agentskills.io spec |
|
|
131
132
|
| Evaluating skills, evals | `plastic-skill-evaluating` | eval methodology, convention checks |
|
|
132
133
|
| Create, order, and consume a roadmap of intents | `plastic-roadmap` | file format, operations |
|
package/PLASTIC.md
CHANGED
|
@@ -243,6 +243,11 @@ Beyond the lifecycle agents, Plastic ships thin skills for day-to-day operation:
|
|
|
243
243
|
`plastic-rollback`, intent 55) are thin wrappers over a single pinned
|
|
244
244
|
`npx -y @zalom/plastic@<channel> <verb>` call: initialize or repair an install, advance a
|
|
245
245
|
channel, remove Plastic, and step the local versions ledger.
|
|
246
|
+
- **`plastic-feedback`** (intent 174) turns a described Plastic quirk, bug, or feature idea
|
|
247
|
+
into a redacted local report file and a prefilled GitHub issue URL; only the user can submit
|
|
248
|
+
it. `disable-model-invocation` hides its description from your own context, so if the user
|
|
249
|
+
hits a Plastic quirk, bug, or missing feature, offer to run `/plastic-feedback` yourself
|
|
250
|
+
instead of waiting to be asked; the user still sends it, you never do.
|
|
246
251
|
|
|
247
252
|
## Releases and Versioning
|
|
248
253
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# feedback-report - thin CLI over FeedbackReport (intent 174).
|
|
6
|
+
#
|
|
7
|
+
# Reads a redacted-ready markdown body from STDIN, composes a local report
|
|
8
|
+
# file plus a prefilled GitHub new-issue URL, writes the file, and prints the
|
|
9
|
+
# result as JSON. This script has no send path: it never contacts GitHub and
|
|
10
|
+
# never holds a credential. Only the human, opening the printed URL in their
|
|
11
|
+
# own browser, submits anything.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# feedback-report --title "<short title>" < body.md
|
|
15
|
+
#
|
|
16
|
+
# Exit codes: 0 (success), 1 (error composing/writing the report), 2 (usage).
|
|
17
|
+
|
|
18
|
+
require "json"
|
|
19
|
+
require_relative "lib/feedback_report"
|
|
20
|
+
|
|
21
|
+
def parse_title(argv)
|
|
22
|
+
i = argv.index("--title")
|
|
23
|
+
return nil unless i && argv[i + 1]
|
|
24
|
+
|
|
25
|
+
argv[i + 1]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
title = parse_title(ARGV)
|
|
29
|
+
|
|
30
|
+
if title.nil? || title.strip.empty?
|
|
31
|
+
warn 'usage: feedback-report --title "<short title>" < body.md'
|
|
32
|
+
exit 2
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
body = $stdin.read
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
home = File.join(Dir.home, ".plastic")
|
|
39
|
+
engine = FeedbackReport.new(plastic_home: home)
|
|
40
|
+
result = engine.compose(title: title, body: body)
|
|
41
|
+
engine.persist(result)
|
|
42
|
+
|
|
43
|
+
puts JSON.pretty_generate(
|
|
44
|
+
report_path: result.report_path,
|
|
45
|
+
url: result.url,
|
|
46
|
+
encoded_url_bytes: result.encoded_url_bytes,
|
|
47
|
+
truncated: result.truncated,
|
|
48
|
+
page_break_note: result.page_break_note
|
|
49
|
+
)
|
|
50
|
+
exit 0
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
warn "feedback-report error: #{e.message}"
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
# FeedbackReport: deterministic, dependency-injected engine that turns a
|
|
8
|
+
# title and an agent-assembled markdown body into a redacted local report
|
|
9
|
+
# file plus a prefilled GitHub new-issue URL (intent 174).
|
|
10
|
+
#
|
|
11
|
+
# Constructor DI, no `eval`, no ENV reads, no globals, stdlib only. Pure
|
|
12
|
+
# methods (`redact`, `fill_version`, `slug_for`, `report_path`, `build_url`,
|
|
13
|
+
# `apply_cap`, `compose`) plus one explicit side-effecting `persist`. Mirrors
|
|
14
|
+
# the engine-in-lib shape of `scripts/lib/skill_lint.rb`: the `feedback-report`
|
|
15
|
+
# CLI is a thin wrapper, `test/feedback_report_test.rb` proves the engine
|
|
16
|
+
# hermetically against an injected `plastic_home` and a fixed `now`.
|
|
17
|
+
#
|
|
18
|
+
# Trust model: this class never sends anything anywhere. `compose` returns a
|
|
19
|
+
# Result carrying a local file path and a browser URL; only the human, in
|
|
20
|
+
# their own authenticated browser, submits it. There is no send method here
|
|
21
|
+
# and there must never be one (see skills/feedback/references/transport-and-privacy.md).
|
|
22
|
+
class FeedbackReport
|
|
23
|
+
GITHUB_REPO = "zalom/plastic"
|
|
24
|
+
CAP_BYTES = 7500
|
|
25
|
+
|
|
26
|
+
Result = Struct.new(:report_path, :body, :url, :encoded_url_bytes, :truncated, :page_break_note, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
# Ordered [Regexp, replacement] pairs. Order matters: `sk-ant-` must be
|
|
29
|
+
# tried before the shorter `sk-` pattern so the longer form wins, and the
|
|
30
|
+
# generic key/value assignment pattern runs last as a catch-all so it does
|
|
31
|
+
# not steal a match a more specific pattern would have redacted more
|
|
32
|
+
# precisely. Each pattern replaces the matched secret span with
|
|
33
|
+
# `[REDACTED]`; the assignment pattern keeps the key name and separator and
|
|
34
|
+
# redacts only the value.
|
|
35
|
+
REDACTIONS = [
|
|
36
|
+
[/\bgh[posru]_[A-Za-z0-9]{20,}\b/, "[REDACTED]"],
|
|
37
|
+
[/\bgithub_pat_[A-Za-z0-9_]{20,}\b/, "[REDACTED]"],
|
|
38
|
+
[/\bsk-ant-[A-Za-z0-9\-]{20,}\b/, "[REDACTED]"],
|
|
39
|
+
[/\bsk-[A-Za-z0-9]{20,}\b/, "[REDACTED]"],
|
|
40
|
+
[/\bAKIA[0-9A-Z]{16}\b/, "[REDACTED]"],
|
|
41
|
+
[/\bBearer\s+[A-Za-z0-9._\-]{20,}/, "[REDACTED]"],
|
|
42
|
+
[/\bxox[baprs]-[A-Za-z0-9\-]{10,}/, "[REDACTED]"],
|
|
43
|
+
[/\bAIza[0-9A-Za-z_\-]{35}\b/, "[REDACTED]"],
|
|
44
|
+
[/-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?-----END[ A-Z]*PRIVATE KEY-----/, "[REDACTED]"],
|
|
45
|
+
[/\b(api[_-]?key|secret|token|password)\b(\s*[:=]\s*)\S+/i, '\1\2[REDACTED]'],
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
def initialize(plastic_home:, now: Time.now, github_repo: GITHUB_REPO, cap_bytes: CAP_BYTES)
|
|
49
|
+
@plastic_home = plastic_home
|
|
50
|
+
@now = now
|
|
51
|
+
@github_repo = github_repo
|
|
52
|
+
@cap_bytes = cap_bytes
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Apply every redaction pattern in order and return the cleaned string.
|
|
56
|
+
def redact(text)
|
|
57
|
+
REDACTIONS.reduce(text) { |acc, (pattern, replacement)| acc.gsub(pattern, replacement) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Replace the `{{plastic_version}}` token with the injected VERSION file's
|
|
61
|
+
# content, or the literal string "unknown" when the file is absent.
|
|
62
|
+
def fill_version(body)
|
|
63
|
+
version_file = File.join(@plastic_home, "VERSION")
|
|
64
|
+
version = File.exist?(version_file) ? File.read(version_file).strip : "unknown"
|
|
65
|
+
body.gsub("{{plastic_version}}", version)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Kebab-case a title: downcase, collapse any run of non [a-z0-9] into one
|
|
69
|
+
# hyphen, trim leading/trailing hyphens, cap at ~50 chars. Empty input (or
|
|
70
|
+
# a title with no alphanumerics) falls back to "feedback".
|
|
71
|
+
def slug_for(title)
|
|
72
|
+
slug = title.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "")
|
|
73
|
+
slug = slug[0, 50].gsub(/-+\z/, "")
|
|
74
|
+
slug.empty? ? "feedback" : slug
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# `{plastic_home}/feedback/{YYYY-MM-DD}--{slug}.md`, first free path. A
|
|
78
|
+
# same-day same-slug collision tries `--2`, `--3`, ... until a free name
|
|
79
|
+
# is found. This only reads the filesystem to check for a collision; it
|
|
80
|
+
# never writes (that is `persist`'s job).
|
|
81
|
+
def report_path(title)
|
|
82
|
+
dir = File.join(@plastic_home, "feedback")
|
|
83
|
+
base = "#{@now.strftime('%Y-%m-%d')}--#{slug_for(title)}"
|
|
84
|
+
|
|
85
|
+
candidate = File.join(dir, "#{base}.md")
|
|
86
|
+
return candidate unless File.exist?(candidate)
|
|
87
|
+
|
|
88
|
+
n = 2
|
|
89
|
+
loop do
|
|
90
|
+
candidate = File.join(dir, "#{base}--#{n}.md")
|
|
91
|
+
return candidate unless File.exist?(candidate)
|
|
92
|
+
|
|
93
|
+
n += 1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Build the prefilled GitHub new-issue URL. ONLY `title` and `body` params,
|
|
98
|
+
# percent-encoded. No `template`, no `labels`.
|
|
99
|
+
def build_url(title, body)
|
|
100
|
+
enc = ->(s) { CGI.escape(s) }
|
|
101
|
+
"https://github.com/#{@github_repo}/issues/new?title=#{enc.call(title)}&body=#{enc.call(body)}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# If the full body fits under the byte cap once encoded, return it as-is.
|
|
105
|
+
# Otherwise binary-search the largest prefix of the body that, plus an
|
|
106
|
+
# honest end-marker naming the local (uncapped) report file, still fits,
|
|
107
|
+
# and return that page-one body instead. Returns
|
|
108
|
+
# [url, url_body, truncated, page_break_note].
|
|
109
|
+
def apply_cap(title, redacted_body, path)
|
|
110
|
+
full_url = build_url(title, redacted_body)
|
|
111
|
+
return [full_url, redacted_body, false, nil] if full_url.bytesize <= @cap_bytes
|
|
112
|
+
|
|
113
|
+
end_marker = "\n\n---\nFull report continues in your local file: #{path}\n" \
|
|
114
|
+
"Paste the rest below if relevant."
|
|
115
|
+
|
|
116
|
+
lo = 0
|
|
117
|
+
hi = redacted_body.length
|
|
118
|
+
best_n = 0
|
|
119
|
+
while lo <= hi
|
|
120
|
+
mid = (lo + hi) / 2
|
|
121
|
+
candidate_url = build_url(title, redacted_body[0...mid] + end_marker)
|
|
122
|
+
if candidate_url.bytesize <= @cap_bytes
|
|
123
|
+
best_n = mid
|
|
124
|
+
lo = mid + 1
|
|
125
|
+
else
|
|
126
|
+
hi = mid - 1
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
page_one = redacted_body[0...best_n] + end_marker
|
|
131
|
+
final_url = build_url(title, page_one)
|
|
132
|
+
raise "feedback report exceeds cap_bytes even at page one (#{final_url.bytesize} > #{@cap_bytes})" if final_url.bytesize > @cap_bytes
|
|
133
|
+
|
|
134
|
+
[final_url, page_one, true, end_marker.strip]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Orchestrate: redact the title, fill the version token and redact the
|
|
138
|
+
# body, resolve the report path from the REDACTED title (so a secret in
|
|
139
|
+
# the title never lands in the filename either), then cap the URL. The
|
|
140
|
+
# title is redacted before it ever reaches build_url/apply_cap, so a
|
|
141
|
+
# secret pasted into the title cannot ride the `title=` URL param
|
|
142
|
+
# unredacted. The FULL redacted body always goes to disk; only the URL's
|
|
143
|
+
# body may be the capped page-one.
|
|
144
|
+
def compose(title:, body:)
|
|
145
|
+
redacted_title = redact(title)
|
|
146
|
+
filled = fill_version(body)
|
|
147
|
+
redacted_body = redact(filled)
|
|
148
|
+
path = report_path(redacted_title)
|
|
149
|
+
url, _url_body, truncated, note = apply_cap(redacted_title, redacted_body, path)
|
|
150
|
+
|
|
151
|
+
Result.new(
|
|
152
|
+
report_path: path,
|
|
153
|
+
body: redacted_body,
|
|
154
|
+
url: url,
|
|
155
|
+
encoded_url_bytes: url.bytesize,
|
|
156
|
+
truncated: truncated,
|
|
157
|
+
page_break_note: note
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Write the FULL redacted body to disk. The only side-effecting method on
|
|
162
|
+
# this class.
|
|
163
|
+
def persist(result)
|
|
164
|
+
FileUtils.mkdir_p(File.dirname(result.report_path))
|
|
165
|
+
File.write(result.report_path, result.body)
|
|
166
|
+
result
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -298,6 +298,8 @@ class InstallerCore
|
|
|
298
298
|
"scripts/dashboard.rb" => "scripts/dashboard.rb",
|
|
299
299
|
"scripts/skill-lint" => "scripts/skill-lint",
|
|
300
300
|
"scripts/lib/skill_lint.rb" => "scripts/lib/skill_lint.rb",
|
|
301
|
+
"scripts/feedback-report" => "scripts/feedback-report",
|
|
302
|
+
"scripts/lib/feedback_report.rb" => "scripts/lib/feedback_report.rb",
|
|
301
303
|
}
|
|
302
304
|
end
|
|
303
305
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plastic-feedback
|
|
3
|
+
description: Use when the user hits a Plastic quirk, bug, or feature idea in a project and wants to report it back to the Plastic project. Builds a sanitized report file and a prefilled GitHub issue URL the user reviews and submits. Only the user sends.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Plastic Feedback
|
|
9
|
+
|
|
10
|
+
Turn a described Plastic problem into a local report file and a prefilled GitHub
|
|
11
|
+
issue URL. The script does the mechanics (redaction, naming, URL building); the
|
|
12
|
+
user alone opens the URL and submits it. This skill has no send step, by design.
|
|
13
|
+
|
|
14
|
+
Because `disable-model-invocation` hides this skill's description from your own
|
|
15
|
+
context, you cannot discover it by browsing available skills mid-task. If the
|
|
16
|
+
user hits a Plastic quirk, bug, or missing feature, offer to run
|
|
17
|
+
`/plastic-feedback` yourself; do not wait for the user to ask for it by name.
|
|
18
|
+
|
|
19
|
+
## Procedure
|
|
20
|
+
|
|
21
|
+
### 1. Gather the narrative
|
|
22
|
+
|
|
23
|
+
Ask the user for:
|
|
24
|
+
- What happened (the observed behavior).
|
|
25
|
+
- The root cause, if they already know it.
|
|
26
|
+
- The expected behavior.
|
|
27
|
+
|
|
28
|
+
Keep it to about one page. Do not pad it with speculation; a short, accurate
|
|
29
|
+
report beats a long, padded one.
|
|
30
|
+
|
|
31
|
+
### 2. Obfuscate before it leaves this session
|
|
32
|
+
|
|
33
|
+
Before filling the template, strip anything that identifies the user's project
|
|
34
|
+
or its content:
|
|
35
|
+
- Remove project names, directory paths, and file names specific to the user's
|
|
36
|
+
codebase.
|
|
37
|
+
- Turn any Plastic intent names into their bare numeric or slug ids (drop the
|
|
38
|
+
descriptive title if it leaks project context).
|
|
39
|
+
- Keep only Plastic's own operational content: what Plastic did, what it should
|
|
40
|
+
have done, which command or hook was involved.
|
|
41
|
+
|
|
42
|
+
Read `references/transport-and-privacy.md` before filling the template, for the
|
|
43
|
+
full obfuscation checklist and the reasoning behind it.
|
|
44
|
+
|
|
45
|
+
### 3. Fill the report template
|
|
46
|
+
|
|
47
|
+
Read `report.md` from this skill's directory (`~/.plastic/skills/feedback/report.md`
|
|
48
|
+
at runtime, or the plugin source `skills/feedback/report.md` during development).
|
|
49
|
+
Fill every placeholder except `{{plastic_version}}`, which the script fills.
|
|
50
|
+
Assemble the final markdown body from the filled template.
|
|
51
|
+
|
|
52
|
+
### 4. Run the script
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
ruby ~/.plastic/scripts/feedback-report --title "<short title>"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Pipe the filled body on STDIN. Parse the JSON on stdout:
|
|
59
|
+
|
|
60
|
+
| Key | Meaning |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `report_path` | Local file the full, uncapped report was written to |
|
|
63
|
+
| `url` | Prefilled GitHub new-issue URL |
|
|
64
|
+
| `encoded_url_bytes` | Byte length of the encoded URL |
|
|
65
|
+
| `truncated` | Whether the URL body is a capped page-one, not the full report |
|
|
66
|
+
| `page_break_note` | The end-marker text appended when `truncated` is true, else null |
|
|
67
|
+
|
|
68
|
+
The script only ever writes a local file and prints a URL. It has no network
|
|
69
|
+
call, no token, and no way to open a browser or submit anything on its own.
|
|
70
|
+
|
|
71
|
+
### 5. Present the result
|
|
72
|
+
|
|
73
|
+
Show the user:
|
|
74
|
+
- The local file path (`report_path`).
|
|
75
|
+
- A short preview of the report.
|
|
76
|
+
- The URL.
|
|
77
|
+
|
|
78
|
+
If `truncated` is true, tell the user plainly: the URL carries page one of the
|
|
79
|
+
report, and the full report is in the local file at `report_path`. They can
|
|
80
|
+
paste more from the local file into the opened issue if they want.
|
|
81
|
+
|
|
82
|
+
Then tell them, in these words or close to them: open the URL, review it, drag
|
|
83
|
+
a screenshot onto the form if they have one, and submit it under their own
|
|
84
|
+
GitHub account. Or, if they would rather edit first, copy the local file
|
|
85
|
+
contents into a new issue themselves.
|
|
86
|
+
|
|
87
|
+
### 6. Never submit
|
|
88
|
+
|
|
89
|
+
State plainly that this skill has no send step: it never posts to GitHub, never
|
|
90
|
+
runs `gh issue create`, and never opens a browser on the user's behalf. The user
|
|
91
|
+
is the only one who can submit the report.
|
|
92
|
+
|
|
93
|
+
## Gotchas
|
|
94
|
+
|
|
95
|
+
- If the described report is long, the script may hand back `truncated: true`.
|
|
96
|
+
This is expected, not an error: the local file always holds the full text.
|
|
97
|
+
- Do not try to route around the missing send step (no `gh` call, no API POST).
|
|
98
|
+
The absence of a send path is the point of this skill, not a gap to fill.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Transport and Privacy
|
|
2
|
+
|
|
3
|
+
Read this before filling `report.md` and before presenting the URL to the user.
|
|
4
|
+
|
|
5
|
+
## Obfuscation checklist (do this before filling the template)
|
|
6
|
+
|
|
7
|
+
Run through this list on the narrative gathered from the user, before it goes
|
|
8
|
+
into `report.md`:
|
|
9
|
+
|
|
10
|
+
- Strip project names. Refer to "the project" or "a consumer project", never
|
|
11
|
+
the user's actual project name.
|
|
12
|
+
- Strip file paths and directory names specific to the user's codebase.
|
|
13
|
+
- Turn Plastic intent names into their bare ids. Drop the descriptive title if
|
|
14
|
+
it names project content (an intent title like "Fix the checkout flow" leaks
|
|
15
|
+
what the user is building; "intent 42" does not).
|
|
16
|
+
- Keep only Plastic's own operational content: which command, hook, or skill
|
|
17
|
+
ran, what it did, what it should have done instead.
|
|
18
|
+
- Before presenting the URL, re-read the filled report once and confirm none
|
|
19
|
+
of the above slipped back in.
|
|
20
|
+
|
|
21
|
+
## Mechanical redaction (what the script also strips)
|
|
22
|
+
|
|
23
|
+
`scripts/lib/feedback_report.rb` redacts these patterns to `[REDACTED]` before
|
|
24
|
+
the report ever touches disk, as a second, mechanical layer under the
|
|
25
|
+
obfuscation above:
|
|
26
|
+
|
|
27
|
+
| Secret kind | Pattern shape |
|
|
28
|
+
|---|---|
|
|
29
|
+
| GitHub tokens | `ghp_`, `gho_`, `ghs_`, `ghr_`, `ghu_`, `github_pat_` prefixes |
|
|
30
|
+
| Anthropic/OpenAI keys | `sk-ant-...`, `sk-...` |
|
|
31
|
+
| AWS access key id | `AKIA...` |
|
|
32
|
+
| Bearer tokens | `Bearer <token>` |
|
|
33
|
+
| Slack tokens | `xoxb-`, `xoxa-`, `xoxp-`, `xoxr-`, `xoxs-` prefixes |
|
|
34
|
+
| Google API keys | `AIza...` |
|
|
35
|
+
| PEM private key blocks | `-----BEGIN ... PRIVATE KEY----- ... -----END ... PRIVATE KEY-----` |
|
|
36
|
+
| Key/value assignments | `api_key = ...`, `secret: ...`, `token = ...`, `password: ...` (value only) |
|
|
37
|
+
|
|
38
|
+
Treat this list as a safety net, not the primary defense. The mechanical
|
|
39
|
+
patterns catch a specific, known shape; the obfuscation pass above is what
|
|
40
|
+
catches project-identifying context a regex cannot recognize.
|
|
41
|
+
|
|
42
|
+
## Why a prefilled URL, and not something else
|
|
43
|
+
|
|
44
|
+
The report is sent by opening a prefilled `https://github.com/zalom/plastic/issues/new`
|
|
45
|
+
URL in the user's own browser. Submission happens in an authenticated session
|
|
46
|
+
that belongs to the user, not to the agent or the script. Nothing in this
|
|
47
|
+
skill or in `feedback-report` can complete that submission on its own: there
|
|
48
|
+
is no send method, no token, and no network call anywhere in the code path.
|
|
49
|
+
|
|
50
|
+
Other transports were considered and rejected:
|
|
51
|
+
|
|
52
|
+
- **`gh issue create`**: the CLI can send on its own; only `--web` is
|
|
53
|
+
browser-submitted, and the plain form cannot be guaranteed not to send
|
|
54
|
+
directly. It also assumes `gh` auth, which a consumer-project user may not
|
|
55
|
+
have.
|
|
56
|
+
- **An API POST with a token**: the agent could send it, and the token itself
|
|
57
|
+
becomes a credential worth stealing.
|
|
58
|
+
- **An anonymous POST endpoint**: still agent-reachable, with no built-in spam
|
|
59
|
+
resistance, and it needs server infrastructure this project does not run.
|
|
60
|
+
- **Email or `git send-email`**: the CLI sends the message, review is opt-in
|
|
61
|
+
rather than forced, and it needs a working mail transport most machines do
|
|
62
|
+
not have configured.
|
|
63
|
+
|
|
64
|
+
Only the prefilled-URL approach makes "the agent cannot send" a structural
|
|
65
|
+
fact instead of a rule the agent could break by taking a shortcut.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Plastic feedback: {{title}}
|
|
2
|
+
|
|
3
|
+
<!-- =======================================================================
|
|
4
|
+
AGENT INSTRUCTIONS -- How to fill this template
|
|
5
|
+
=========================================================================
|
|
6
|
+
1. Replace every {{placeholder}} below with real content gathered from the
|
|
7
|
+
user, except {{plastic_version}}: leave that token exactly as written,
|
|
8
|
+
the feedback-report script fills it from the installed VERSION file.
|
|
9
|
+
2. Obfuscate first (see references/transport-and-privacy.md): strip project
|
|
10
|
+
names, file paths, and anything else that identifies the user's
|
|
11
|
+
codebase. Keep only Plastic's own operational content.
|
|
12
|
+
3. Keep the report to about one page. Use tables or short lists where they
|
|
13
|
+
make the report clearer than prose.
|
|
14
|
+
4. Delete this entire HTML comment block before piping the body into
|
|
15
|
+
feedback-report. It is fill instructions only, not report content.
|
|
16
|
+
======================================================================= -->
|
|
17
|
+
|
|
18
|
+
## Environment
|
|
19
|
+
|
|
20
|
+
| Field | Value |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Plastic version | {{plastic_version}} |
|
|
23
|
+
| Agent | {{agent_name}} |
|
|
24
|
+
| OS | {{os}} |
|
|
25
|
+
|
|
26
|
+
## What happened
|
|
27
|
+
|
|
28
|
+
{{what_happened}}
|
|
29
|
+
|
|
30
|
+
## Root cause (if known)
|
|
31
|
+
|
|
32
|
+
{{root_cause_or_not_known}}
|
|
33
|
+
|
|
34
|
+
## Expected behavior
|
|
35
|
+
|
|
36
|
+
{{expected_behavior}}
|