@poa-box/agent 0.1.0
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/.env.agent.template +20 -0
- package/README.md +46 -0
- package/brain/Config/agent-config.json +14 -0
- package/brain/Config/brain-allowlist.json +20 -0
- package/brain/Identity/goals.template.md +23 -0
- package/brain/Identity/how-i-think.md +406 -0
- package/brain/Identity/who-i-am.template.md +34 -0
- package/brain/Knowledge/BOOTSTRAP.md +66 -0
- package/brain/Knowledge/audit-corpus-index.json +406 -0
- package/brain/Knowledge/discussions.json +245 -0
- package/brain/Knowledge/pop.brain.brainstorms.generated.md +48 -0
- package/brain/Knowledge/pop.brain.brainstorms.genesis.bin +0 -0
- package/brain/Knowledge/pop.brain.heuristics.snapshot.bin +0 -0
- package/brain/Knowledge/pop.brain.projects.generated.md +16 -0
- package/brain/Knowledge/pop.brain.projects.genesis.bin +0 -0
- package/brain/Knowledge/pop.brain.retros.generated.md +91 -0
- package/brain/Knowledge/pop.brain.retros.genesis.bin +0 -0
- package/brain/Knowledge/pop.brain.shared.generated.md +3811 -0
- package/brain/Knowledge/pop.brain.shared.genesis.bin +0 -0
- package/brain/Knowledge/projects.md +181 -0
- package/brain/Knowledge/risk-framework.md +90 -0
- package/brain/Knowledge/shared.md +416 -0
- package/brain/Knowledge/sprint-priorities.md +439 -0
- package/brain/Memory/.gitkeep +0 -0
- package/dist/commands/agent/daily-digest.d.ts +24 -0
- package/dist/commands/agent/daily-digest.js +336 -0
- package/dist/commands/agent/delegate.d.ts +12 -0
- package/dist/commands/agent/delegate.js +91 -0
- package/dist/commands/agent/deploy-to-org.d.ts +20 -0
- package/dist/commands/agent/deploy-to-org.js +154 -0
- package/dist/commands/agent/index.d.ts +2 -0
- package/dist/commands/agent/index.js +27 -0
- package/dist/commands/agent/init.d.ts +19 -0
- package/dist/commands/agent/init.js +303 -0
- package/dist/commands/agent/onboard.d.ts +22 -0
- package/dist/commands/agent/onboard.js +192 -0
- package/dist/commands/agent/paymaster-status.d.ts +14 -0
- package/dist/commands/agent/paymaster-status.js +130 -0
- package/dist/commands/agent/register.d.ts +21 -0
- package/dist/commands/agent/register.js +116 -0
- package/dist/commands/agent/setup-sponsorship.d.ts +22 -0
- package/dist/commands/agent/setup-sponsorship.js +154 -0
- package/dist/commands/agent/status.d.ts +12 -0
- package/dist/commands/agent/status.js +171 -0
- package/dist/commands/agent/triage.d.ts +12 -0
- package/dist/commands/agent/triage.js +503 -0
- package/dist/commands/brain/advance-stage.d.ts +42 -0
- package/dist/commands/brain/advance-stage.js +206 -0
- package/dist/commands/brain/allowlist.d.ts +30 -0
- package/dist/commands/brain/allowlist.js +274 -0
- package/dist/commands/brain/append-lesson.d.ts +55 -0
- package/dist/commands/brain/append-lesson.js +245 -0
- package/dist/commands/brain/brainstorm.d.ts +154 -0
- package/dist/commands/brain/brainstorm.js +573 -0
- package/dist/commands/brain/daemon.d.ts +31 -0
- package/dist/commands/brain/daemon.js +348 -0
- package/dist/commands/brain/doctor.d.ts +27 -0
- package/dist/commands/brain/doctor.js +497 -0
- package/dist/commands/brain/edit-lesson.d.ts +51 -0
- package/dist/commands/brain/edit-lesson.js +248 -0
- package/dist/commands/brain/import-snapshot.d.ts +68 -0
- package/dist/commands/brain/import-snapshot.js +177 -0
- package/dist/commands/brain/index.d.ts +2 -0
- package/dist/commands/brain/index.js +67 -0
- package/dist/commands/brain/list.d.ts +21 -0
- package/dist/commands/brain/list.js +83 -0
- package/dist/commands/brain/migrate-projects.d.ts +44 -0
- package/dist/commands/brain/migrate-projects.js +209 -0
- package/dist/commands/brain/migrate.d.ts +74 -0
- package/dist/commands/brain/migrate.js +306 -0
- package/dist/commands/brain/new-project.d.ts +53 -0
- package/dist/commands/brain/new-project.js +226 -0
- package/dist/commands/brain/read.d.ts +24 -0
- package/dist/commands/brain/read.js +81 -0
- package/dist/commands/brain/remove-lesson.d.ts +47 -0
- package/dist/commands/brain/remove-lesson.js +206 -0
- package/dist/commands/brain/remove-project.d.ts +36 -0
- package/dist/commands/brain/remove-project.js +177 -0
- package/dist/commands/brain/retro-file-tasks.d.ts +84 -0
- package/dist/commands/brain/retro-file-tasks.js +372 -0
- package/dist/commands/brain/retro-list.d.ts +28 -0
- package/dist/commands/brain/retro-list.js +125 -0
- package/dist/commands/brain/retro-mark-change.d.ts +58 -0
- package/dist/commands/brain/retro-mark-change.js +176 -0
- package/dist/commands/brain/retro-remove.d.ts +36 -0
- package/dist/commands/brain/retro-remove.js +142 -0
- package/dist/commands/brain/retro-respond.d.ts +56 -0
- package/dist/commands/brain/retro-respond.js +250 -0
- package/dist/commands/brain/retro-show.d.ts +23 -0
- package/dist/commands/brain/retro-show.js +100 -0
- package/dist/commands/brain/retro-start.d.ts +55 -0
- package/dist/commands/brain/retro-start.js +311 -0
- package/dist/commands/brain/search.d.ts +48 -0
- package/dist/commands/brain/search.js +190 -0
- package/dist/commands/brain/snapshot.d.ts +32 -0
- package/dist/commands/brain/snapshot.js +243 -0
- package/dist/commands/brain/status.d.ts +15 -0
- package/dist/commands/brain/status.js +166 -0
- package/dist/commands/brain/subscribe.d.ts +28 -0
- package/dist/commands/brain/subscribe.js +90 -0
- package/dist/commands/brain/tag.d.ts +46 -0
- package/dist/commands/brain/tag.js +192 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +22 -0
- package/dist/lib/brain-daemon.d.ts +126 -0
- package/dist/lib/brain-daemon.js +811 -0
- package/dist/lib/brain-membership.d.ts +58 -0
- package/dist/lib/brain-membership.js +115 -0
- package/dist/lib/brain-migrate-projects.d.ts +43 -0
- package/dist/lib/brain-migrate-projects.js +247 -0
- package/dist/lib/brain-migrate.d.ts +77 -0
- package/dist/lib/brain-migrate.js +328 -0
- package/dist/lib/brain-ops.d.ts +271 -0
- package/dist/lib/brain-ops.js +571 -0
- package/dist/lib/brain-paths.d.ts +15 -0
- package/dist/lib/brain-paths.js +33 -0
- package/dist/lib/brain-projections.d.ts +216 -0
- package/dist/lib/brain-projections.js +829 -0
- package/dist/lib/brain-schemas.d.ts +36 -0
- package/dist/lib/brain-schemas.js +316 -0
- package/dist/lib/brain-signing.d.ts +103 -0
- package/dist/lib/brain-signing.js +256 -0
- package/dist/lib/brain.d.ts +198 -0
- package/dist/lib/brain.js +1057 -0
- package/dist/pop-agent.d.ts +1 -0
- package/dist/pop-agent.js +18 -0
- package/docs/agent.md +126 -0
- package/docs/agents/brain-anti-entropy.md +127 -0
- package/docs/agents/brain-cross-device-onboarding.md +210 -0
- package/docs/agents/brain-cross-machine-smoke.md +241 -0
- package/docs/agents/brain-layer-setup.md +725 -0
- package/docs/agents/offboarding-protocol.md +188 -0
- package/docs/agents/onboarding-protocol.md +243 -0
- package/docs/agents/running-an-agent.md +200 -0
- package/docs/brain.md +560 -0
- package/package.json +61 -0
- package/scripts/apply.sh +140 -0
- package/scripts/onboard.sh +205 -0
- package/scripts/setup-agent.ts +272 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain doc schemas — write-time shape validation (Task #346, HB#168).
|
|
3
|
+
*
|
|
4
|
+
* Retro #1 change #5: currently applyBrainChange() accepts any doc shape
|
|
5
|
+
* and the projection layer (brain-projections.ts) tolerates bad shapes at
|
|
6
|
+
* read time via formatTimestamp() and schema-tolerant renderers. That
|
|
7
|
+
* approach means bad data enters the doc and stays forever (Automerge
|
|
8
|
+
* field renames are merge hazards per HB#248). This module validates
|
|
9
|
+
* shape at WRITE time so canonically-broken entries never enter the doc.
|
|
10
|
+
*
|
|
11
|
+
* DESIGN:
|
|
12
|
+
* - Per-doc-id validators. pop.brain.shared, pop.brain.projects,
|
|
13
|
+
* pop.brain.retros each have their own shape. Unknown doc ids return
|
|
14
|
+
* `{ ok: true, warnings: [...] }` — permissionless schema evolution.
|
|
15
|
+
* - Validators check ITEMS of known arrays for required fields. Extra
|
|
16
|
+
* fields are allowed (extensibility); missing required fields are errors.
|
|
17
|
+
* - applyBrainChange diffs pre-vs-post validity: if the pre-change doc
|
|
18
|
+
* was already invalid, the bad state was inherited, not introduced, and
|
|
19
|
+
* the new write is NOT rejected. Only regressions (valid → invalid) are
|
|
20
|
+
* rejected. This preserves the constraint "existing bad entries stay
|
|
21
|
+
* readable" while still preventing new bad writes.
|
|
22
|
+
* - Per-command --allow-invalid-shape bypass lives at the CLI layer and is
|
|
23
|
+
* plumbed through applyBrainChange's options bag.
|
|
24
|
+
*
|
|
25
|
+
* SCHEMAS ARE DEFINED ONCE HERE and NOT duplicated in the CLI commands.
|
|
26
|
+
*/
|
|
27
|
+
export interface ValidationResult {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
errors: string[];
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Dispatch entry point. Returns { ok, errors, warnings }. Unknown doc ids
|
|
34
|
+
* are permitted (schema evolution) with a warning, not an error.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateBrainDocShape(docId: string, doc: any): ValidationResult;
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Brain doc schemas — write-time shape validation (Task #346, HB#168).
|
|
4
|
+
*
|
|
5
|
+
* Retro #1 change #5: currently applyBrainChange() accepts any doc shape
|
|
6
|
+
* and the projection layer (brain-projections.ts) tolerates bad shapes at
|
|
7
|
+
* read time via formatTimestamp() and schema-tolerant renderers. That
|
|
8
|
+
* approach means bad data enters the doc and stays forever (Automerge
|
|
9
|
+
* field renames are merge hazards per HB#248). This module validates
|
|
10
|
+
* shape at WRITE time so canonically-broken entries never enter the doc.
|
|
11
|
+
*
|
|
12
|
+
* DESIGN:
|
|
13
|
+
* - Per-doc-id validators. pop.brain.shared, pop.brain.projects,
|
|
14
|
+
* pop.brain.retros each have their own shape. Unknown doc ids return
|
|
15
|
+
* `{ ok: true, warnings: [...] }` — permissionless schema evolution.
|
|
16
|
+
* - Validators check ITEMS of known arrays for required fields. Extra
|
|
17
|
+
* fields are allowed (extensibility); missing required fields are errors.
|
|
18
|
+
* - applyBrainChange diffs pre-vs-post validity: if the pre-change doc
|
|
19
|
+
* was already invalid, the bad state was inherited, not introduced, and
|
|
20
|
+
* the new write is NOT rejected. Only regressions (valid → invalid) are
|
|
21
|
+
* rejected. This preserves the constraint "existing bad entries stay
|
|
22
|
+
* readable" while still preventing new bad writes.
|
|
23
|
+
* - Per-command --allow-invalid-shape bypass lives at the CLI layer and is
|
|
24
|
+
* plumbed through applyBrainChange's options bag.
|
|
25
|
+
*
|
|
26
|
+
* SCHEMAS ARE DEFINED ONCE HERE and NOT duplicated in the CLI commands.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.validateBrainDocShape = validateBrainDocShape;
|
|
30
|
+
/**
|
|
31
|
+
* Lesson schema used by pop.brain.shared and (historically) pop.brain.lessons.
|
|
32
|
+
* Canonical shape: { id, author, title, body, timestamp, removed? }.
|
|
33
|
+
* Legacy tolerance: `text` is accepted as a synonym for `body`, `ts` for
|
|
34
|
+
* `timestamp` — the projection layer reads both (HB#297 formatTimestamp).
|
|
35
|
+
* A lesson must have at least one of {body, text} AND at least one of
|
|
36
|
+
* {title, id}. That's the minimum renderable shape.
|
|
37
|
+
*/
|
|
38
|
+
function validateLesson(lesson, index, errors) {
|
|
39
|
+
if (lesson == null || typeof lesson !== 'object') {
|
|
40
|
+
errors.push(`lessons[${index}]: not an object`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const hasBody = (typeof lesson.body === 'string' && lesson.body.length > 0) ||
|
|
44
|
+
(typeof lesson.text === 'string' && lesson.text.length > 0);
|
|
45
|
+
if (!hasBody) {
|
|
46
|
+
errors.push(`lessons[${index}]: missing required body/text (one must be a non-empty string)`);
|
|
47
|
+
}
|
|
48
|
+
const hasTitle = (typeof lesson.title === 'string' && lesson.title.length > 0) ||
|
|
49
|
+
(typeof lesson.id === 'string' && lesson.id.length > 0);
|
|
50
|
+
if (!hasTitle) {
|
|
51
|
+
errors.push(`lessons[${index}]: missing required title/id (one must be a non-empty string)`);
|
|
52
|
+
}
|
|
53
|
+
if (lesson.timestamp != null && lesson.ts != null) {
|
|
54
|
+
// Not an error; just unusual. Don't warn noisily.
|
|
55
|
+
}
|
|
56
|
+
if (lesson.timestamp != null &&
|
|
57
|
+
typeof lesson.timestamp !== 'number' &&
|
|
58
|
+
typeof lesson.timestamp !== 'string') {
|
|
59
|
+
errors.push(`lessons[${index}]: timestamp must be number or ISO string`);
|
|
60
|
+
}
|
|
61
|
+
// Task #347: optional tags field. Must be an array of strings when present.
|
|
62
|
+
// Backwards compatible: existing lessons without tags still validate.
|
|
63
|
+
// Tag vocabulary is free-form — no enforcement of category:/topic:/etc.
|
|
64
|
+
if (lesson.tags != null) {
|
|
65
|
+
if (!Array.isArray(lesson.tags)) {
|
|
66
|
+
errors.push(`lessons[${index}]: tags must be an array of strings`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
for (let i = 0; i < lesson.tags.length; i++) {
|
|
70
|
+
if (typeof lesson.tags[i] !== 'string') {
|
|
71
|
+
errors.push(`lessons[${index}]: tags[${i}] must be a string`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function validateRule(rule, index, errors) {
|
|
78
|
+
if (rule == null || typeof rule !== 'object') {
|
|
79
|
+
errors.push(`rules[${index}]: not an object`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (typeof rule.text !== 'string' || rule.text.length === 0) {
|
|
83
|
+
errors.push(`rules[${index}]: missing required text (non-empty string)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* pop.brain.shared — lessons + rules + operatingConstraints + orgState.
|
|
88
|
+
* Arrays are optional (first-write bootstrap) but when present their items
|
|
89
|
+
* must validate.
|
|
90
|
+
*/
|
|
91
|
+
function validateSharedDoc(doc, errors, warnings) {
|
|
92
|
+
if (doc == null || typeof doc !== 'object') {
|
|
93
|
+
errors.push('pop.brain.shared: doc is not an object');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (doc.lessons != null) {
|
|
97
|
+
if (!Array.isArray(doc.lessons)) {
|
|
98
|
+
errors.push('pop.brain.shared: lessons must be an array');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
doc.lessons.forEach((l, i) => validateLesson(l, i, errors));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (doc.rules != null) {
|
|
105
|
+
if (!Array.isArray(doc.rules)) {
|
|
106
|
+
errors.push('pop.brain.shared: rules must be an array');
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
doc.rules.forEach((r, i) => validateRule(r, i, errors));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (doc.operatingConstraints != null && !Array.isArray(doc.operatingConstraints)) {
|
|
113
|
+
errors.push('pop.brain.shared: operatingConstraints must be an array');
|
|
114
|
+
}
|
|
115
|
+
if (doc.orgState != null && typeof doc.orgState !== 'object') {
|
|
116
|
+
errors.push('pop.brain.shared: orgState must be an object');
|
|
117
|
+
}
|
|
118
|
+
void warnings;
|
|
119
|
+
}
|
|
120
|
+
// HB#183 (lesson cross-module-enum-drift): the source of truth for the
|
|
121
|
+
// project lifecycle stages is the ProjectStage union type in
|
|
122
|
+
// src/lib/brain-projections.ts. We MUST NOT retype the values here —
|
|
123
|
+
// drift between the schema and the projection breaks at runtime
|
|
124
|
+
// (HB#180 incident: schema enum was {proposed, building, ...},
|
|
125
|
+
// canonical was {propose, discuss, ...}, validator rejected legitimate
|
|
126
|
+
// new-project writes). The literal-array-as-type pattern below lets
|
|
127
|
+
// TypeScript keep both the runtime Set and the union type in sync from
|
|
128
|
+
// one declaration. If the canonical lifecycle ever changes,
|
|
129
|
+
// brain-projections.ts ProjectStage stays the source of truth — update
|
|
130
|
+
// that union, then update PROJECT_STAGES here in lockstep, and the
|
|
131
|
+
// vitest "accepts all canonical lifecycle stages" case fails loudly
|
|
132
|
+
// if drift creeps back in.
|
|
133
|
+
// The literal tuple is what gives us a runtime Set; the satisfies clause
|
|
134
|
+
// below proves at compile time that this list is structurally identical
|
|
135
|
+
// to the ProjectStage union from brain-projections. If brain-projections
|
|
136
|
+
// adds or removes a stage and this list isn't updated, tsc fails the
|
|
137
|
+
// build at the satisfies line — drift is impossible.
|
|
138
|
+
const PROJECT_STAGES = ['propose', 'discuss', 'plan', 'vote', 'execute', 'review', 'ship'];
|
|
139
|
+
// Compile-time bidirectional drift check.
|
|
140
|
+
// Direction 1 (literal → union): every literal in PROJECT_STAGES must
|
|
141
|
+
// be a valid ProjectStage. tsc fails if you add a typo to the tuple.
|
|
142
|
+
const _stagesAreValid = PROJECT_STAGES;
|
|
143
|
+
const _stagesMatchUnion = true;
|
|
144
|
+
void _stagesAreValid;
|
|
145
|
+
void _stagesMatchUnion;
|
|
146
|
+
const VALID_PROJECT_STAGES = new Set(PROJECT_STAGES);
|
|
147
|
+
function validateProject(p, index, errors) {
|
|
148
|
+
if (p == null || typeof p !== 'object') {
|
|
149
|
+
errors.push(`projects[${index}]: not an object`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (typeof p.id !== 'string' || p.id.length === 0) {
|
|
153
|
+
errors.push(`projects[${index}]: missing required id (non-empty string)`);
|
|
154
|
+
}
|
|
155
|
+
if (typeof p.name !== 'string' || p.name.length === 0) {
|
|
156
|
+
errors.push(`projects[${index}]: missing required name (non-empty string)`);
|
|
157
|
+
}
|
|
158
|
+
if (typeof p.stage !== 'string' || !VALID_PROJECT_STAGES.has(p.stage)) {
|
|
159
|
+
errors.push(`projects[${index}]: stage must be one of ${[...VALID_PROJECT_STAGES].join('|')}, got ${JSON.stringify(p.stage)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function validateProjectsDoc(doc, errors) {
|
|
163
|
+
if (doc == null || typeof doc !== 'object') {
|
|
164
|
+
errors.push('pop.brain.projects: doc is not an object');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (doc.projects != null) {
|
|
168
|
+
if (!Array.isArray(doc.projects)) {
|
|
169
|
+
errors.push('pop.brain.projects: projects must be an array');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
doc.projects.forEach((p, i) => validateProject(p, i, errors));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function validateRetro(r, index, errors) {
|
|
177
|
+
if (r == null || typeof r !== 'object') {
|
|
178
|
+
errors.push(`retros[${index}]: not an object`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (typeof r.id !== 'string' || r.id.length === 0) {
|
|
182
|
+
errors.push(`retros[${index}]: missing required id`);
|
|
183
|
+
}
|
|
184
|
+
if (r.proposedChanges != null && !Array.isArray(r.proposedChanges)) {
|
|
185
|
+
errors.push(`retros[${index}]: proposedChanges must be an array`);
|
|
186
|
+
}
|
|
187
|
+
if (r.discussion != null && !Array.isArray(r.discussion)) {
|
|
188
|
+
errors.push(`retros[${index}]: discussion must be an array`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function validateRetrosDoc(doc, errors) {
|
|
192
|
+
if (doc == null || typeof doc !== 'object') {
|
|
193
|
+
errors.push('pop.brain.retros: doc is not an object');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (doc.retros != null) {
|
|
197
|
+
if (!Array.isArray(doc.retros)) {
|
|
198
|
+
errors.push('pop.brain.retros: retros must be an array');
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
doc.retros.forEach((r, i) => validateRetro(r, i, errors));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// --- pop.brain.brainstorms (task #354 phase a, HB#207) -------------
|
|
206
|
+
//
|
|
207
|
+
// Brainstorms are a forward-looking cross-agent ideation surface:
|
|
208
|
+
// { id, title, prompt, author, status, ideas[], window, removed? }.
|
|
209
|
+
// Distinct from pop.brain.retros (reactive session retrospectives)
|
|
210
|
+
// and pop.brain.projects (lifecycle state machine) — this doc is
|
|
211
|
+
// where new questions get posted, ideas get debated + voted, and
|
|
212
|
+
// top-ranked ideas get promoted to pop.brain.projects at the propose
|
|
213
|
+
// stage. See docs/agents/brain-layer-setup.md and the #354 task description
|
|
214
|
+
// for the full lifecycle.
|
|
215
|
+
const VALID_BRAINSTORM_STATUSES = ['open', 'voting', 'closed', 'promoted'];
|
|
216
|
+
const VALID_BRAINSTORM_STATUS_SET = new Set(VALID_BRAINSTORM_STATUSES);
|
|
217
|
+
const VALID_VOTE_STANCES = ['support', 'explore', 'oppose'];
|
|
218
|
+
const VALID_VOTE_STANCE_SET = new Set(VALID_VOTE_STANCES);
|
|
219
|
+
function validateIdea(idea, brainstormId, ideaIndex, errors) {
|
|
220
|
+
if (idea == null || typeof idea !== 'object') {
|
|
221
|
+
errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: not an object`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (typeof idea.id !== 'string' || idea.id.length === 0) {
|
|
225
|
+
errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: missing required id`);
|
|
226
|
+
}
|
|
227
|
+
if (typeof idea.message !== 'string' || idea.message.length === 0) {
|
|
228
|
+
errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: missing required message`);
|
|
229
|
+
}
|
|
230
|
+
if (idea.author != null && typeof idea.author !== 'string') {
|
|
231
|
+
errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: author must be a string when present`);
|
|
232
|
+
}
|
|
233
|
+
if (idea.votes != null) {
|
|
234
|
+
if (typeof idea.votes !== 'object' || Array.isArray(idea.votes)) {
|
|
235
|
+
errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: votes must be an object keyed by agent address`);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
for (const [addr, stance] of Object.entries(idea.votes)) {
|
|
239
|
+
if (typeof stance !== 'string' || !VALID_VOTE_STANCE_SET.has(stance)) {
|
|
240
|
+
errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}].votes[${addr}]: stance must be one of ${[...VALID_VOTE_STANCES].join('|')}, got ${JSON.stringify(stance)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (idea.priority != null && !['high', 'medium', 'low'].includes(idea.priority)) {
|
|
246
|
+
errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: priority must be high|medium|low when present`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function validateBrainstorm(b, index, errors) {
|
|
250
|
+
if (b == null || typeof b !== 'object') {
|
|
251
|
+
errors.push(`brainstorms[${index}]: not an object`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const bid = b.id ?? `<index ${index}>`;
|
|
255
|
+
if (typeof b.id !== 'string' || b.id.length === 0) {
|
|
256
|
+
errors.push(`brainstorms[${index}]: missing required id`);
|
|
257
|
+
}
|
|
258
|
+
if (typeof b.title !== 'string' || b.title.length === 0) {
|
|
259
|
+
errors.push(`brainstorms[${bid}]: missing required title`);
|
|
260
|
+
}
|
|
261
|
+
if (typeof b.status !== 'string' || !VALID_BRAINSTORM_STATUS_SET.has(b.status)) {
|
|
262
|
+
errors.push(`brainstorms[${bid}]: status must be one of ${[...VALID_BRAINSTORM_STATUSES].join('|')}, got ${JSON.stringify(b.status)}`);
|
|
263
|
+
}
|
|
264
|
+
if (b.ideas != null) {
|
|
265
|
+
if (!Array.isArray(b.ideas)) {
|
|
266
|
+
errors.push(`brainstorms[${bid}]: ideas must be an array`);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
b.ideas.forEach((idea, i) => validateIdea(idea, bid, i, errors));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (b.promotedToProjectIds != null && !Array.isArray(b.promotedToProjectIds)) {
|
|
273
|
+
errors.push(`brainstorms[${bid}]: promotedToProjectIds must be an array`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function validateBrainstormsDoc(doc, errors) {
|
|
277
|
+
if (doc == null || typeof doc !== 'object') {
|
|
278
|
+
errors.push('pop.brain.brainstorms: doc is not an object');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (doc.brainstorms != null) {
|
|
282
|
+
if (!Array.isArray(doc.brainstorms)) {
|
|
283
|
+
errors.push('pop.brain.brainstorms: brainstorms must be an array');
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
doc.brainstorms.forEach((b, i) => validateBrainstorm(b, i, errors));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Dispatch entry point. Returns { ok, errors, warnings }. Unknown doc ids
|
|
292
|
+
* are permitted (schema evolution) with a warning, not an error.
|
|
293
|
+
*/
|
|
294
|
+
function validateBrainDocShape(docId, doc) {
|
|
295
|
+
const errors = [];
|
|
296
|
+
const warnings = [];
|
|
297
|
+
switch (docId) {
|
|
298
|
+
case 'pop.brain.shared':
|
|
299
|
+
case 'pop.brain.lessons':
|
|
300
|
+
validateSharedDoc(doc, errors, warnings);
|
|
301
|
+
break;
|
|
302
|
+
case 'pop.brain.projects':
|
|
303
|
+
validateProjectsDoc(doc, errors);
|
|
304
|
+
break;
|
|
305
|
+
case 'pop.brain.retros':
|
|
306
|
+
validateRetrosDoc(doc, errors);
|
|
307
|
+
break;
|
|
308
|
+
case 'pop.brain.brainstorms':
|
|
309
|
+
validateBrainstormsDoc(doc, errors);
|
|
310
|
+
break;
|
|
311
|
+
default:
|
|
312
|
+
warnings.push(`unknown doc id "${docId}" — no schema registered, accepting any shape. ` +
|
|
313
|
+
`Add a validator to src/lib/brain-schemas.ts to enforce one.`);
|
|
314
|
+
}
|
|
315
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
316
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain-layer change signing — ECDSA over snapshot bytes.
|
|
3
|
+
*
|
|
4
|
+
* Each brain CRDT change gets wrapped in a signed envelope before being
|
|
5
|
+
* written to the blockstore. The envelope's sig authenticates the full
|
|
6
|
+
* Automerge snapshot against an Ethereum address derived from the
|
|
7
|
+
* existing POP_PRIVATE_KEY. No Nostr keys, no Schnorr, no second PKI.
|
|
8
|
+
*
|
|
9
|
+
* Envelope format (v1):
|
|
10
|
+
*
|
|
11
|
+
* {
|
|
12
|
+
* v: 1,
|
|
13
|
+
* author: "0xABCD...", // Ethereum address (lowercase)
|
|
14
|
+
* timestamp: 1776200000, // unix seconds
|
|
15
|
+
* automerge: "0xDEADBEEF", // full Automerge.save() bytes, hex
|
|
16
|
+
* sig: "0xABC..." // ECDSA over keccak256(author|ts|automerge)
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Serialized as UTF-8 JSON, stored as a raw-codec IPLD block. The CID
|
|
20
|
+
* covers the whole envelope, so the sig is content-addressed alongside
|
|
21
|
+
* the data it authenticates.
|
|
22
|
+
*
|
|
23
|
+
* On read, the projection layer:
|
|
24
|
+
* 1. Unmarshal the envelope JSON
|
|
25
|
+
* 2. Verify the sig recovers to `author`
|
|
26
|
+
* 3. Check `author` against the allowlist at
|
|
27
|
+
* agent/brain/Config/brain-allowlist.json
|
|
28
|
+
* 4. If all OK, extract the Automerge bytes and merge
|
|
29
|
+
*
|
|
30
|
+
* Sync layer stays permissionless — any peer can gossip any CID. Auth
|
|
31
|
+
* happens at read time so the network is resilient against relay
|
|
32
|
+
* operators (there are none, but the principle stands for any future
|
|
33
|
+
* transport).
|
|
34
|
+
*/
|
|
35
|
+
export interface BrainChangeEnvelope {
|
|
36
|
+
v: 1;
|
|
37
|
+
author: string;
|
|
38
|
+
timestamp: number;
|
|
39
|
+
automerge: string;
|
|
40
|
+
sig: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sign an Automerge snapshot with the wallet derived from POP_PRIVATE_KEY.
|
|
44
|
+
* Returns a v1 envelope ready to be JSON-encoded and written as a block.
|
|
45
|
+
*/
|
|
46
|
+
export declare function signBrainChange(automergeBytes: Uint8Array, privateKey?: string): Promise<BrainChangeEnvelope>;
|
|
47
|
+
/**
|
|
48
|
+
* Verify an envelope's signature and return the recovered author.
|
|
49
|
+
* Throws if the envelope is malformed or the signature doesn't verify.
|
|
50
|
+
*
|
|
51
|
+
* NOTE: this only checks authenticity (sig corresponds to `author`);
|
|
52
|
+
* it does NOT check authorization (whether `author` is allowed to
|
|
53
|
+
* write to this doc). That's the allowlist check — see isAllowedAuthor.
|
|
54
|
+
*/
|
|
55
|
+
export declare function verifyBrainChange(envelope: BrainChangeEnvelope): string;
|
|
56
|
+
/**
|
|
57
|
+
* Extract the Automerge snapshot bytes from an envelope.
|
|
58
|
+
* Does NOT verify the signature — caller must run verifyBrainChange first.
|
|
59
|
+
*/
|
|
60
|
+
export declare function unwrapAutomergeBytes(envelope: BrainChangeEnvelope): Uint8Array;
|
|
61
|
+
export interface AllowlistEntry {
|
|
62
|
+
address: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
addedAt?: string;
|
|
65
|
+
addedBy?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Path to the git-tracked brain-allowlist.json. Single source of truth
|
|
69
|
+
* for who is permitted to write to brain docs. Edited via governance
|
|
70
|
+
* (or via `pop brain allowlist add/remove`, which writes to the same
|
|
71
|
+
* file and leaves the git review gate in place).
|
|
72
|
+
*
|
|
73
|
+
* Exported so command handlers can write to the same path without
|
|
74
|
+
* hard-coding it independently.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getAllowlistPath(): string;
|
|
77
|
+
export declare function loadAllowlist(): AllowlistEntry[];
|
|
78
|
+
/**
|
|
79
|
+
* Check whether a given address is in the allowlist.
|
|
80
|
+
* Case-insensitive on the address.
|
|
81
|
+
*/
|
|
82
|
+
export declare function isAllowedAuthor(address: string): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Combined auth check: verify signature + allowlist membership.
|
|
85
|
+
* Returns the authenticated author on success; throws otherwise.
|
|
86
|
+
*/
|
|
87
|
+
export declare function authenticateAndAuthorize(envelope: BrainChangeEnvelope): string;
|
|
88
|
+
export type AuthorizationMode = 'dynamic' | 'static-fallback' | 'both-agree';
|
|
89
|
+
export interface AuthorizationResult {
|
|
90
|
+
allowed: boolean;
|
|
91
|
+
mode: AuthorizationMode;
|
|
92
|
+
/** Populated only when fallback fired. Empty string otherwise. */
|
|
93
|
+
fallbackReason: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Async authorization check: on-chain org membership first, static
|
|
97
|
+
* JSON allowlist second. Does NOT throw — returns a result object the
|
|
98
|
+
* caller can inspect for logging purposes. Callers then decide whether
|
|
99
|
+
* to reject the change or accept it based on `.allowed`.
|
|
100
|
+
*
|
|
101
|
+
* This is the new canonical authorization for brain read paths.
|
|
102
|
+
*/
|
|
103
|
+
export declare function isAuthorizedAuthor(address: string): Promise<AuthorizationResult>;
|