nobrainer-tech-flow 0.0.0-stage → 2.4.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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.codex-plugin/plugin.json +47 -0
- package/.opencode/INSTALL.md +37 -0
- package/.opencode/plugins/nobrainer-tech-skills.js +66 -0
- package/.pi/extensions/nobrainer-tech-skills.js +64 -0
- package/AGENTS.md +204 -0
- package/CLAUDE.md +204 -0
- package/CONTRIBUTING.md +70 -0
- package/GEMINI.md +1 -0
- package/LICENSE +21 -0
- package/README.md +658 -2
- package/SECURITY.md +33 -0
- package/adapters/bootstrap.md +27 -0
- package/assets/nobrainer-tech-logo.svg +1 -0
- package/assets/nobrainer-workflow.svg +155 -0
- package/assets/plugin-logo.png +0 -0
- package/bin/nobrainer-tech-flow.mjs +48 -0
- package/docs/BOUNDED_RUNNER.md +60 -0
- package/docs/CHAT_PLUGINS.md +29 -0
- package/docs/COMPATIBILITY.md +371 -0
- package/docs/INSTALL.md +626 -0
- package/docs/MIGRATION_TO_FLOW.md +117 -0
- package/docs/NAMING.md +39 -0
- package/docs/PRIVACY.md +9 -0
- package/docs/SESSION_RESTART.md +70 -0
- package/docs/SKILL_CURATION.md +124 -0
- package/docs/TESTING.md +232 -0
- package/docs/TRY_IT.md +109 -0
- package/docs/releases/v1.0.0.md +81 -0
- package/docs/releases/v1.1.0.md +85 -0
- package/docs/releases/v1.10.0.md +34 -0
- package/docs/releases/v1.11.0-publication-readback.md +25 -0
- package/docs/releases/v1.11.0.md +42 -0
- package/docs/releases/v1.12.0.md +30 -0
- package/docs/releases/v1.13.0.md +46 -0
- package/docs/releases/v1.14.0.md +43 -0
- package/docs/releases/v1.14.1.md +21 -0
- package/docs/releases/v1.2.0.md +87 -0
- package/docs/releases/v1.2.1.md +92 -0
- package/docs/releases/v1.3.0-publication-readback.md +69 -0
- package/docs/releases/v1.3.0.md +66 -0
- package/docs/releases/v1.3.1-publication-readback.md +76 -0
- package/docs/releases/v1.3.1.md +59 -0
- package/docs/releases/v1.4.0-publication-readback.md +81 -0
- package/docs/releases/v1.4.0.md +43 -0
- package/docs/releases/v1.5.0-publication-readback.md +76 -0
- package/docs/releases/v1.5.0.md +54 -0
- package/docs/releases/v1.6.0.md +64 -0
- package/docs/releases/v1.6.1-publication-readback.md +53 -0
- package/docs/releases/v1.6.1.md +82 -0
- package/docs/releases/v1.7.0.md +42 -0
- package/docs/releases/v1.7.1-publication-readback.md +64 -0
- package/docs/releases/v1.7.1.md +49 -0
- package/docs/releases/v1.8.0.md +63 -0
- package/docs/releases/v1.8.1.md +32 -0
- package/docs/releases/v1.9.0.md +33 -0
- package/docs/releases/v2.0.0-candidate.md +28 -0
- package/docs/releases/v2.0.0.md +53 -0
- package/docs/releases/v2.0.1.md +135 -0
- package/docs/releases/v2.0.2.md +189 -0
- package/docs/releases/v2.1.0.md +64 -0
- package/docs/releases/v2.1.1.md +43 -0
- package/docs/releases/v2.2.0.md +64 -0
- package/docs/releases/v2.2.1.md +11 -0
- package/docs/releases/v2.3.0.md +97 -0
- package/docs/releases/v2.3.1.md +71 -0
- package/docs/releases/v2.3.2.md +242 -0
- package/docs/releases/v2.4.0.md +126 -0
- package/gemini-extension.json +6 -0
- package/hooks/claude-hooks.json +17 -0
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/run-hook.cmd +36 -0
- package/hooks/session-start +38 -0
- package/package.json +69 -4
- package/plugin.json +51 -0
- package/scripts/build_plugin.py +50 -0
- package/scripts/check_flow_update.py +18 -0
- package/scripts/check_public_commit.py +86 -0
- package/scripts/fetch_public_refs.py +69 -0
- package/scripts/install.py +600 -0
- package/scripts/install.sh +1392 -0
- package/scripts/install_guards.py +279 -0
- package/scripts/install_personalization.py +527 -0
- package/scripts/install_skills.py +951 -0
- package/scripts/npm_install.py +85 -0
- package/scripts/recommend_flow_setup.py +1522 -0
- package/scripts/validate_skills.py +465 -0
- package/skills/nobrainer-auto-fine-tune/SKILL.md +79 -0
- package/skills/nobrainer-auto-fine-tune/references/capacity-audit.md +49 -0
- package/skills/nobrainer-autoimprove/SKILL.md +212 -0
- package/skills/nobrainer-autoimprove/scripts/count_words.py +22 -0
- package/skills/nobrainer-browser/SKILL.md +234 -0
- package/skills/nobrainer-browser/references/cdp-profile-restart.md +93 -0
- package/skills/nobrainer-build/SKILL.md +173 -0
- package/skills/nobrainer-codex-context/SKILL.md +111 -0
- package/skills/nobrainer-codex-context/scripts/codex_context.py +175 -0
- package/skills/nobrainer-decide/SKILL.md +198 -0
- package/skills/nobrainer-dispatcher/SKILL.md +256 -0
- package/skills/nobrainer-dispatcher/agents/openai.yaml +4 -0
- package/skills/nobrainer-rca/SKILL.md +163 -0
- package/skills/nobrainer-research/SKILL.md +131 -0
- package/skills/nobrainer-review/SKILL.md +195 -0
- package/skills/nobrainer-security/SKILL.md +166 -0
- package/skills/nobrainer-sessions/SKILL.md +223 -0
- package/skills/nobrainer-sessions/references/evidence-receipt.md +55 -0
- package/skills/nobrainer-sessions/references/protocol.md +337 -0
- package/skills/nobrainer-sessions/references/session-restart.md +255 -0
- package/skills/nobrainer-sessions/scripts/evidence_receipt.py +373 -0
- package/skills/nobrainer-sessions/scripts/restart_gate.py +140 -0
- package/skills/nobrainer-sessions/scripts/run_bounded.py +390 -0
- package/skills/nobrainer-sessions/scripts/session_title.py +43 -0
- package/skills/nobrainer-skill-doctor/SKILL.md +149 -0
- package/skills/nobrainer-skill-doctor/references/astra.md +19 -0
- package/skills/nobrainer-skill-doctor/scripts/audit.py +100 -0
- package/skills/nobrainer-skill-doctor/scripts/context_candidates.py +133 -0
- package/skills/nobrainer-spec-driven-development/SKILL.md +132 -0
- package/skills/nobrainer-spec-driven-development/references/spec-template.md +114 -0
- package/skills/nobrainer-team/SKILL.md +165 -0
- package/skills/nobrainer-team/references/team-plan.md +64 -0
- package/skills/nobrainer-tech-flow/SKILL.md +239 -0
- package/skills/nobrainer-tech-flow/VERSION +1 -0
- package/skills/nobrainer-tech-flow/references/autopilot.md +72 -0
- package/skills/nobrainer-tech-flow/references/codex-routing.md +21 -0
- package/skills/nobrainer-tech-flow/references/communication.md +108 -0
- package/skills/nobrainer-tech-flow/references/correction-hooks.md +110 -0
- package/skills/nobrainer-tech-flow/references/daily-update.md +28 -0
- package/skills/nobrainer-tech-flow/references/delivery.md +243 -0
- package/skills/nobrainer-tech-flow/references/idea-to-delivery.md +82 -0
- package/skills/nobrainer-tech-flow/references/long-run-state.md +209 -0
- package/skills/nobrainer-tech-flow/references/model-routing.md +83 -0
- package/skills/nobrainer-tech-flow/references/optional-decisions.md +83 -0
- package/skills/nobrainer-tech-flow/references/project-start.md +22 -0
- package/skills/nobrainer-tech-flow/references/routing.md +125 -0
- package/skills/nobrainer-tech-flow/references/setup.md +180 -0
- package/skills/nobrainer-tech-flow/references/worktrees.md +138 -0
- package/skills/nobrainer-tech-flow/scripts/check_flow_update.py +298 -0
- package/skills/nobrainer-tech-flow/scripts/cleanup_worktree.py +327 -0
- package/skills/nobrainer-tech-flow/scripts/decision_config.py +107 -0
- package/skills/nobrainer-tech-flow/scripts/flow_guard.py +252 -0
- package/skills/nobrainer-tech-flow/scripts/typed_decisions.py +210 -0
- package/skills/nobrainer-wiki/SKILL.md +164 -0
- package/skills/nobrainer-wiki/references/setup.md +88 -0
- package/skills/nobrainer-writing/SKILL.md +272 -0
- package/skills/nobrainer-writing/agents/openai.yaml +4 -0
- package/skills/nobrainer-writing/references/brief-artifacts.md +357 -0
- package/skills/nobrainer-writing/references/plain-technical-english.md +76 -0
- package/skills/nobrainer-writing/references/research.md +86 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Local, unsigned execution evidence for an explicit input scope.
|
|
3
|
+
|
|
4
|
+
Capture reuses run_bounded; check never executes the recorded command. Neither
|
|
5
|
+
mode decides test coverage, task acceptance or human approval.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import errno
|
|
11
|
+
import base64
|
|
12
|
+
import hashlib
|
|
13
|
+
import html
|
|
14
|
+
import importlib.util
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path, PurePosixPath
|
|
18
|
+
import stat
|
|
19
|
+
import secrets
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
MAX_ENTRIES = 1000
|
|
23
|
+
MAX_INPUT_BYTES = 64 * 1024 * 1024
|
|
24
|
+
MAX_RECEIPT_BYTES = 4 * 1024 * 1024
|
|
25
|
+
MAX_OUTPUT_BYTES = 256 * 1024
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EvidenceError(ValueError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def digest(data: bytes) -> str:
|
|
33
|
+
return hashlib.sha256(data).hexdigest()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def relative_path(value: str) -> str:
|
|
37
|
+
if not isinstance(value, str) or not value or len(value.encode('utf-8')) > 512:
|
|
38
|
+
raise EvidenceError('invalid relative input path')
|
|
39
|
+
if '\\' in value or any(ord(c) < 32 for c in value):
|
|
40
|
+
raise EvidenceError('unsafe input path')
|
|
41
|
+
path = PurePosixPath(value)
|
|
42
|
+
if path.is_absolute() or '..' in path.parts or str(path) != value or len(path.parts) > 32:
|
|
43
|
+
raise EvidenceError('input paths must be normalized and relative to root')
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def scope_paths(values: object) -> list[str]:
|
|
48
|
+
if not isinstance(values, list) or not 1 <= len(values) <= 64:
|
|
49
|
+
raise EvidenceError('input scope must contain 1..64 relative paths')
|
|
50
|
+
paths = [relative_path(p) for p in values]
|
|
51
|
+
if len(set(paths)) != len(paths):
|
|
52
|
+
raise EvidenceError('duplicate input scope')
|
|
53
|
+
return sorted(paths)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def require_posix() -> None:
|
|
57
|
+
if os.name != 'posix' or not hasattr(os, 'O_NOFOLLOW'):
|
|
58
|
+
raise EvidenceError('unsupported platform: POSIX no-follow file access is required')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def snapshot(root: Path, scope: list[str]) -> dict[str, dict[str, object]]:
|
|
62
|
+
"""Walk selected entries through directory descriptors, never through links."""
|
|
63
|
+
require_posix()
|
|
64
|
+
entries: dict[str, dict[str, object]] = {}
|
|
65
|
+
total = 0
|
|
66
|
+
file_count = 0
|
|
67
|
+
directory_flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
|
|
68
|
+
root_fd = os.open(root, directory_flags)
|
|
69
|
+
|
|
70
|
+
def visit(parent_fd: int, name: str, relative: str, depth: int) -> None:
|
|
71
|
+
nonlocal total, file_count
|
|
72
|
+
if relative in entries:
|
|
73
|
+
return
|
|
74
|
+
relative_path(relative)
|
|
75
|
+
if depth > 32 or len(entries) >= MAX_ENTRIES:
|
|
76
|
+
raise EvidenceError('input entry/depth limit exceeded')
|
|
77
|
+
info = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
|
|
78
|
+
if stat.S_ISDIR(info.st_mode):
|
|
79
|
+
fd = os.open(name, directory_flags, dir_fd=parent_fd)
|
|
80
|
+
try:
|
|
81
|
+
entries[relative] = {'type': 'directory', 'mode': stat.S_IMODE(os.fstat(fd).st_mode)}
|
|
82
|
+
with os.scandir(fd) as iterator:
|
|
83
|
+
names = []
|
|
84
|
+
for entry in iterator:
|
|
85
|
+
if len(names) + len(entries) >= MAX_ENTRIES:
|
|
86
|
+
raise EvidenceError('input entry limit exceeded')
|
|
87
|
+
names.append(entry.name)
|
|
88
|
+
names.sort()
|
|
89
|
+
for child in names:
|
|
90
|
+
child_relative = child if relative == '.' else relative + '/' + child
|
|
91
|
+
visit(fd, child, child_relative, depth + 1)
|
|
92
|
+
finally:
|
|
93
|
+
os.close(fd)
|
|
94
|
+
elif stat.S_ISREG(info.st_mode):
|
|
95
|
+
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent_fd)
|
|
96
|
+
try:
|
|
97
|
+
before = os.fstat(fd)
|
|
98
|
+
if not stat.S_ISREG(before.st_mode):
|
|
99
|
+
raise EvidenceError('input is not a regular file')
|
|
100
|
+
checksum = hashlib.sha256(); size = 0
|
|
101
|
+
while True:
|
|
102
|
+
chunk = os.read(fd, 65536)
|
|
103
|
+
if not chunk:
|
|
104
|
+
break
|
|
105
|
+
size += len(chunk); total += len(chunk)
|
|
106
|
+
if total > MAX_INPUT_BYTES:
|
|
107
|
+
raise EvidenceError('input byte limit exceeded')
|
|
108
|
+
checksum.update(chunk)
|
|
109
|
+
after = os.fstat(fd)
|
|
110
|
+
identity = lambda item: (item.st_dev, item.st_ino, item.st_size, item.st_mtime_ns, item.st_ctime_ns)
|
|
111
|
+
if identity(before) != identity(after) or size != after.st_size:
|
|
112
|
+
raise EvidenceError('input changed while being fingerprinted')
|
|
113
|
+
entries[relative] = {'type': 'file', 'mode': stat.S_IMODE(after.st_mode), 'bytes': size, 'sha256': checksum.hexdigest()}
|
|
114
|
+
file_count += 1
|
|
115
|
+
finally:
|
|
116
|
+
os.close(fd)
|
|
117
|
+
else:
|
|
118
|
+
raise EvidenceError('symlinks and special input files are unsupported')
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
for relative in scope:
|
|
122
|
+
parts = PurePosixPath(relative).parts
|
|
123
|
+
parent_fd = os.dup(root_fd)
|
|
124
|
+
try:
|
|
125
|
+
for part in parts[:-1]:
|
|
126
|
+
child_fd = os.open(part, directory_flags, dir_fd=parent_fd)
|
|
127
|
+
os.close(parent_fd); parent_fd = child_fd
|
|
128
|
+
visit(parent_fd, parts[-1] if parts else '.', relative, len(parts))
|
|
129
|
+
finally:
|
|
130
|
+
os.close(parent_fd)
|
|
131
|
+
finally:
|
|
132
|
+
os.close(root_fd)
|
|
133
|
+
if not file_count:
|
|
134
|
+
raise EvidenceError('input scope contains no regular files')
|
|
135
|
+
return dict(sorted(entries.items()))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def load_runner():
|
|
139
|
+
path = Path(__file__).with_name('run_bounded.py')
|
|
140
|
+
spec = importlib.util.spec_from_file_location('flow_evidence_runner', path)
|
|
141
|
+
if spec is None or spec.loader is None:
|
|
142
|
+
raise EvidenceError('bundled bounded runner is unavailable')
|
|
143
|
+
module = importlib.util.module_from_spec(spec)
|
|
144
|
+
spec.loader.exec_module(module)
|
|
145
|
+
return module
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def unique_keys(pairs):
|
|
149
|
+
result = {}
|
|
150
|
+
for key, value in pairs:
|
|
151
|
+
if key in result:
|
|
152
|
+
raise EvidenceError('duplicate JSON object key')
|
|
153
|
+
result[key] = value
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def read_receipt(path: Path) -> dict:
|
|
158
|
+
if path.is_symlink() or not path.is_file():
|
|
159
|
+
raise EvidenceError('receipt must be a regular file, not a symlink')
|
|
160
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
161
|
+
try:
|
|
162
|
+
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
163
|
+
raise EvidenceError('receipt must be a regular file')
|
|
164
|
+
with os.fdopen(fd, 'rb', closefd=False) as handle:
|
|
165
|
+
raw = handle.read(MAX_RECEIPT_BYTES + 1)
|
|
166
|
+
finally:
|
|
167
|
+
os.close(fd)
|
|
168
|
+
if len(raw) > MAX_RECEIPT_BYTES:
|
|
169
|
+
raise EvidenceError('receipt byte limit exceeded')
|
|
170
|
+
try:
|
|
171
|
+
value = json.loads(raw, object_pairs_hook=unique_keys)
|
|
172
|
+
except (ValueError, RecursionError) as exc:
|
|
173
|
+
raise EvidenceError('invalid receipt JSON') from exc
|
|
174
|
+
if not isinstance(value, dict) or type(value.get('schema_version')) is not int or value.get('schema_version') != 1 or value.get('kind') != 'flow-command-evidence':
|
|
175
|
+
raise EvidenceError('unsupported receipt shape')
|
|
176
|
+
return value
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def validate_snapshot(value: object) -> None:
|
|
180
|
+
if not isinstance(value, dict) or not 1 <= len(value) <= MAX_ENTRIES:
|
|
181
|
+
raise EvidenceError('invalid snapshot')
|
|
182
|
+
for name, record in value.items():
|
|
183
|
+
relative_path(name)
|
|
184
|
+
if not isinstance(record, dict) or record.get('type') not in ('file', 'directory'):
|
|
185
|
+
raise EvidenceError('invalid snapshot entry')
|
|
186
|
+
if type(record.get('mode')) is not int or not 0 <= record['mode'] <= 0o7777:
|
|
187
|
+
raise EvidenceError('invalid mode')
|
|
188
|
+
if record['type'] == 'file':
|
|
189
|
+
if type(record.get('bytes')) is not int or not 0 <= record['bytes'] <= MAX_INPUT_BYTES:
|
|
190
|
+
raise EvidenceError('invalid file size')
|
|
191
|
+
if not isinstance(record.get('sha256'), str) or len(record['sha256']) != 64 or any(c not in '0123456789abcdef' for c in record['sha256']):
|
|
192
|
+
raise EvidenceError('invalid fingerprint')
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def assess(value: dict, root: Path) -> dict:
|
|
196
|
+
scope = scope_paths(value.get('input_scope'))
|
|
197
|
+
validate_snapshot(value.get('before'))
|
|
198
|
+
if 'after' not in value:
|
|
199
|
+
raise EvidenceError('missing after snapshot')
|
|
200
|
+
if value.get('after') is not None:
|
|
201
|
+
validate_snapshot(value['after'])
|
|
202
|
+
run = value.get('execution')
|
|
203
|
+
if not isinstance(run, dict) or run.get('status') not in ('completed', 'failed', 'timed_out', 'output_limit'):
|
|
204
|
+
raise EvidenceError('invalid execution record')
|
|
205
|
+
if run.get('exit_code') is not None and type(run['exit_code']) is not int:
|
|
206
|
+
raise EvidenceError('invalid exit code')
|
|
207
|
+
for stream in ('stdout', 'stderr'):
|
|
208
|
+
if type(run.get(stream + '_bytes_captured')) is not int or type(run.get(stream + '_truncated')) is not bool:
|
|
209
|
+
raise EvidenceError('invalid captured-output metadata')
|
|
210
|
+
try:
|
|
211
|
+
output = value['output'][stream]
|
|
212
|
+
encoded = output['base64']
|
|
213
|
+
if not isinstance(encoded, str) or len(encoded) > 4 * MAX_OUTPUT_BYTES:
|
|
214
|
+
raise EvidenceError('invalid output size')
|
|
215
|
+
raw = base64.b64decode(encoded, validate=True)
|
|
216
|
+
if len(raw) > MAX_OUTPUT_BYTES or digest(raw) != output['sha256'] or len(raw) != run[stream + '_bytes_captured']:
|
|
217
|
+
raise EvidenceError('output integrity mismatch')
|
|
218
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
219
|
+
raise EvidenceError('invalid or inconsistent captured output') from exc
|
|
220
|
+
label = value.get('label')
|
|
221
|
+
if not isinstance(label, str) or not 1 <= len(label) <= 120 or any(ord(c) < 32 for c in label):
|
|
222
|
+
raise EvidenceError('invalid check label')
|
|
223
|
+
if run['status'] == 'completed' and (run.get('exit_code') != 0 or run.get('timed_out') is not False or run.get('output_limit_reached') is not False or run.get('signal') is not None or run.get('stdout_truncated') is not False or run.get('stderr_truncated') is not False):
|
|
224
|
+
raise EvidenceError('completed command metadata is inconsistent')
|
|
225
|
+
try:
|
|
226
|
+
current = snapshot(root, scope)
|
|
227
|
+
inputs = 'CURRENT' if current == value['after'] else 'STALE'
|
|
228
|
+
except (OSError, EvidenceError):
|
|
229
|
+
inputs = 'UNAVAILABLE'
|
|
230
|
+
if value['after'] is None:
|
|
231
|
+
inputs = 'UNAVAILABLE'
|
|
232
|
+
elif value['before'] != value['after']:
|
|
233
|
+
inputs = 'CHANGED_DURING_RUN'
|
|
234
|
+
return {
|
|
235
|
+
'label': label,
|
|
236
|
+
'inputs': inputs,
|
|
237
|
+
'input_scope': scope,
|
|
238
|
+
'execution': run['status'],
|
|
239
|
+
'exit_code': run.get('exit_code'),
|
|
240
|
+
'output_integrity': 'CONSISTENT_NOT_AUTHENTICATED',
|
|
241
|
+
'test_counts': 'UNKNOWN',
|
|
242
|
+
'coverage': 'UNKNOWN',
|
|
243
|
+
'task_acceptance': 'NOT_EVALUATED',
|
|
244
|
+
'human_approval': 'NOT_RECORDED',
|
|
245
|
+
'authenticity': 'NOT_VERIFIED',
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def render_card(report: dict) -> str:
|
|
250
|
+
def safe(value):
|
|
251
|
+
return html.escape(str(value)).replace('|', '|').replace('`', '`')
|
|
252
|
+
rows = [('Check', report['label']), ('Input freshness', report['inputs']),
|
|
253
|
+
('Command', report['execution']), ('Exit code', report['exit_code']),
|
|
254
|
+
('Test counts / coverage', 'UNKNOWN'), ('Task acceptance', 'NOT_EVALUATED'),
|
|
255
|
+
('Human approval', 'NOT_RECORDED'), ('Authenticity', 'NOT_VERIFIED')]
|
|
256
|
+
return '# Execution evidence\n\n| Field | Observed scope |\n|---|---|\n' + ''.join(
|
|
257
|
+
f'| {label} | {safe(value)} |\n' for label, value in rows
|
|
258
|
+
) + '\nCheck the logs, actual criteria and selected input scope before accepting the task.\n'
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def write_receipt(parent_fd: int, name: str, encoded: bytes) -> None:
|
|
262
|
+
"""Atomically create evidence in the pinned directory without replacement."""
|
|
263
|
+
temporary = '.flow-evidence-' + secrets.token_hex(16)
|
|
264
|
+
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
|
265
|
+
0o600, dir_fd=parent_fd)
|
|
266
|
+
try:
|
|
267
|
+
with os.fdopen(fd, 'wb') as handle:
|
|
268
|
+
handle.write(encoded)
|
|
269
|
+
handle.flush()
|
|
270
|
+
os.fsync(handle.fileno())
|
|
271
|
+
os.link(temporary, name, src_dir_fd=parent_fd, dst_dir_fd=parent_fd,
|
|
272
|
+
follow_symlinks=False)
|
|
273
|
+
try:
|
|
274
|
+
os.fsync(parent_fd)
|
|
275
|
+
except OSError as exc:
|
|
276
|
+
if exc.errno not in {errno.EINVAL, errno.ENOTSUP, errno.EPERM}:
|
|
277
|
+
raise
|
|
278
|
+
finally:
|
|
279
|
+
os.unlink(temporary, dir_fd=parent_fd)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def capture(args) -> int:
|
|
283
|
+
require_posix()
|
|
284
|
+
runner = load_runner()
|
|
285
|
+
root = args.root.expanduser().resolve(strict=True)
|
|
286
|
+
scope = scope_paths(args.input)
|
|
287
|
+
receipt = args.receipt.expanduser().absolute()
|
|
288
|
+
receipt = receipt.parent.resolve(strict=True) / receipt.name
|
|
289
|
+
if receipt.exists() or receipt.is_symlink():
|
|
290
|
+
raise EvidenceError('receipt already exists; not overwritten')
|
|
291
|
+
if not args.label or len(args.label) > 120 or any(ord(c) < 32 for c in args.label):
|
|
292
|
+
raise EvidenceError('label must be 1..120 printable characters')
|
|
293
|
+
if args.max_output_bytes > MAX_OUTPUT_BYTES:
|
|
294
|
+
raise EvidenceError('maximum captured output is 262144 bytes')
|
|
295
|
+
for relative in scope:
|
|
296
|
+
selected = root / relative
|
|
297
|
+
if receipt == selected or receipt.is_relative_to(selected):
|
|
298
|
+
raise EvidenceError('receipt must be outside every selected input path')
|
|
299
|
+
parent_fd = os.open(receipt.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
300
|
+
try:
|
|
301
|
+
parent_info = os.fstat(parent_fd)
|
|
302
|
+
before = snapshot(root, scope)
|
|
303
|
+
command = args.command[1:] if args.command and args.command[0] == '--' else args.command
|
|
304
|
+
if not command:
|
|
305
|
+
raise EvidenceError('an explicit argv command is required after --')
|
|
306
|
+
run_args = argparse.Namespace(command=command, cwd=root, wall_seconds=args.wall_seconds,
|
|
307
|
+
max_output_bytes=args.max_output_bytes, record_argv=args.record_argv)
|
|
308
|
+
execution, code, captured = runner.run_bounded(run_args)
|
|
309
|
+
# The runner calls a run completed when the command exits 0, even if it had to cut
|
|
310
|
+
# its output short, for example when a detached child keeps the pipe open. Such a
|
|
311
|
+
# run did not deliver all of its output, so it is recorded as an output-limit run.
|
|
312
|
+
if execution.get('status') == 'completed' and (execution.get('stdout_truncated') or execution.get('stderr_truncated')):
|
|
313
|
+
execution = {**execution, 'status': 'output_limit'}
|
|
314
|
+
after = None; snapshot_error = None
|
|
315
|
+
try:
|
|
316
|
+
after = snapshot(root, scope)
|
|
317
|
+
except (OSError, EvidenceError) as exc:
|
|
318
|
+
snapshot_error = type(exc).__name__
|
|
319
|
+
value = {'schema_version': 1, 'kind': 'flow-command-evidence', 'label': args.label,
|
|
320
|
+
'input_scope': scope, 'before': before, 'after': after, 'snapshot_error': snapshot_error,
|
|
321
|
+
'execution': execution,
|
|
322
|
+
'output': {key: {'base64': base64.b64encode(raw).decode('ascii'), 'sha256': digest(raw)} for key, raw in captured.items()}}
|
|
323
|
+
encoded = (json.dumps(value, sort_keys=True, indent=2) + '\n').encode('utf-8')
|
|
324
|
+
if len(encoded) > MAX_RECEIPT_BYTES:
|
|
325
|
+
raise EvidenceError('receipt byte limit exceeded; command ran but receipt could not be recorded')
|
|
326
|
+
try:
|
|
327
|
+
current_parent = os.stat(receipt.parent, follow_symlinks=False)
|
|
328
|
+
except OSError as exc:
|
|
329
|
+
raise EvidenceError('receipt directory unavailable; command ran but evidence was not written') from exc
|
|
330
|
+
if not stat.S_ISDIR(current_parent.st_mode) or (current_parent.st_dev, current_parent.st_ino) != (parent_info.st_dev, parent_info.st_ino):
|
|
331
|
+
raise EvidenceError('receipt directory changed; command ran but evidence was not written')
|
|
332
|
+
# Checked before writing, so a receipt that a later check would reject is never left.
|
|
333
|
+
report = assess(value, root)
|
|
334
|
+
write_receipt(parent_fd, receipt.name, encoded)
|
|
335
|
+
print(json.dumps(report, sort_keys=True))
|
|
336
|
+
# A zero exit is success only for evidence that check would also accept.
|
|
337
|
+
return code if code else (0 if report['inputs'] == 'CURRENT' and report['execution'] == 'completed' else 1)
|
|
338
|
+
finally:
|
|
339
|
+
os.close(parent_fd)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def main(argv=None) -> int:
|
|
343
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
344
|
+
sub = parser.add_subparsers(dest='mode', required=True)
|
|
345
|
+
run = sub.add_parser('capture', help='run an already-authorized command and record local evidence')
|
|
346
|
+
run.add_argument('--root', required=True, type=Path)
|
|
347
|
+
run.add_argument('--input', action='append', required=True)
|
|
348
|
+
run.add_argument('--receipt', required=True, type=Path)
|
|
349
|
+
run.add_argument('--label', required=True)
|
|
350
|
+
run.add_argument('--wall-seconds', required=True, type=lambda value: load_runner().positive_float(value))
|
|
351
|
+
run.add_argument('--max-output-bytes', required=True, type=lambda value: load_runner().positive_int(value))
|
|
352
|
+
run.add_argument('--record-argv', action='store_true')
|
|
353
|
+
run.add_argument('command', nargs=argparse.REMAINDER)
|
|
354
|
+
check = sub.add_parser('check', help='inspect an existing receipt; never execute its command')
|
|
355
|
+
check.add_argument('--root', required=True, type=Path)
|
|
356
|
+
check.add_argument('--receipt', required=True, type=Path)
|
|
357
|
+
check.add_argument('--format', choices=['json', 'markdown'], default='json')
|
|
358
|
+
args = parser.parse_args(argv)
|
|
359
|
+
try:
|
|
360
|
+
if args.mode == 'capture':
|
|
361
|
+
return capture(args)
|
|
362
|
+
require_posix()
|
|
363
|
+
root = args.root.expanduser().resolve(strict=True)
|
|
364
|
+
report = assess(read_receipt(args.receipt), root)
|
|
365
|
+
print(render_card(report) if args.format == 'markdown' else json.dumps(report, sort_keys=True))
|
|
366
|
+
return 0 if report['inputs'] == 'CURRENT' and report['execution'] == 'completed' else 1
|
|
367
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
368
|
+
print(f'evidence_receipt: {exc}', file=sys.stderr)
|
|
369
|
+
return 2
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if __name__ == '__main__':
|
|
373
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Evaluate normalized session observations; never create, send, archive or lock.
|
|
3
|
+
|
|
4
|
+
Optional hook adapters supply verified observations. Missing safety observations
|
|
5
|
+
fail closed. This helper is not a session backend or proof of host capability.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def decide(state: dict) -> dict[str, object]:
|
|
13
|
+
assessment = {}
|
|
14
|
+
def result(action: str, reason: str) -> dict[str, object]:
|
|
15
|
+
return {"action": action, "reason": reason, **({"assessment": assessment} if assessment else {})}
|
|
16
|
+
|
|
17
|
+
def yes(name: str) -> bool:
|
|
18
|
+
return state.get(name) is True
|
|
19
|
+
|
|
20
|
+
def number(name: str) -> float | None:
|
|
21
|
+
value = state.get(name)
|
|
22
|
+
return value if type(value) in (int, float) and 0 <= value < float("inf") else None
|
|
23
|
+
|
|
24
|
+
if type(state.get("schema")) is not int or state["schema"] != 1:
|
|
25
|
+
return result("BLOCKED", "unsupported_schema")
|
|
26
|
+
if not isinstance(state.get("source_id"), str) or not state["source_id"].strip():
|
|
27
|
+
return result("BLOCKED", "source_identity")
|
|
28
|
+
if not isinstance(state.get("goal_id"), str) or not state["goal_id"].strip():
|
|
29
|
+
return result("BLOCKED", "goal_identity")
|
|
30
|
+
target = state.get("target_id")
|
|
31
|
+
if yes("ownership_committed") and (
|
|
32
|
+
not isinstance(target, str) or not target.strip() or target == state["source_id"]
|
|
33
|
+
):
|
|
34
|
+
return result("BLOCKED", "committed_target_identity")
|
|
35
|
+
# After a committed transfer the source must never resume implementation.
|
|
36
|
+
if yes("ownership_committed"):
|
|
37
|
+
if not yes("target_takeover_readback") or not yes("ack_matches"):
|
|
38
|
+
return result("VERIFY_TAKEOVER", "source_remains_readonly")
|
|
39
|
+
if not yes("source_retired_readback"):
|
|
40
|
+
return result("VERIFY_SOURCE_RELEASE", "do_not_archive_an_unreleased_source")
|
|
41
|
+
if state.get("archive_status") == "unknown":
|
|
42
|
+
return result("RECONCILE_ARCHIVE", "target_owns_work_never_recreate")
|
|
43
|
+
if yes("source_archived_readback"):
|
|
44
|
+
return result("COMPLETE", "takeover_and_archive_verified")
|
|
45
|
+
if not yes("archive_authorized") or not yes("archive_supported"):
|
|
46
|
+
return result("ARCHIVE_PENDING", "target_owns_work_source_remains_readonly")
|
|
47
|
+
return result("ARCHIVE_SOURCE", "archive_exact_source_only")
|
|
48
|
+
creation_status = state.get("creation_status")
|
|
49
|
+
if creation_status == "unknown":
|
|
50
|
+
return result("RECONCILE_TARGET", "never_retry_uncertain_create")
|
|
51
|
+
if creation_status not in ("not_sent", "confirmed"):
|
|
52
|
+
return result("BLOCKED", "creation_status")
|
|
53
|
+
target = state.get("target_id")
|
|
54
|
+
if creation_status == "confirmed":
|
|
55
|
+
if not isinstance(target, str) or not target.strip() or target == state["source_id"]:
|
|
56
|
+
return result("BLOCKED", "target_identity")
|
|
57
|
+
elif target:
|
|
58
|
+
return result("RECONCILE_TARGET", "unexpected_target")
|
|
59
|
+
if not yes("remaining_work"):
|
|
60
|
+
return result("FINISH" if state.get("remaining_work") is False else "BLOCKED", "remaining_work")
|
|
61
|
+
if yes("required_budget_unmeasurable"):
|
|
62
|
+
return result("BLOCKED", "required_budget_unmeasurable")
|
|
63
|
+
if state.get("mode") not in ("off", "adaptive", "daily"):
|
|
64
|
+
return result("BLOCKED", "invalid_mode")
|
|
65
|
+
if state["mode"] == "off" or not yes("policy_authorized"):
|
|
66
|
+
return result("CHECKPOINT_ONLY", "restart_not_authorized")
|
|
67
|
+
# A short forecast limits optimistic claims about days of future work.
|
|
68
|
+
current, fresh, overhead, calls = (number(name) for name in (
|
|
69
|
+
"current_input_tokens", "fresh_input_tokens", "restart_overhead_tokens", "remaining_calls"
|
|
70
|
+
))
|
|
71
|
+
measured = all(value is not None for value in (current, fresh, overhead, calls))
|
|
72
|
+
pays_back = False
|
|
73
|
+
if measured:
|
|
74
|
+
horizon = min(calls, 3)
|
|
75
|
+
avoided = (current - fresh) * horizon
|
|
76
|
+
pays_back = horizon >= 1 and avoided > 2 * overhead
|
|
77
|
+
assessment = {
|
|
78
|
+
"basis": "estimated_token_proxy_not_billing",
|
|
79
|
+
"horizon_calls": horizon,
|
|
80
|
+
"estimated_net_tokens_saved": avoided - overhead,
|
|
81
|
+
"pays_back_with_margin": pays_back,
|
|
82
|
+
}
|
|
83
|
+
# Pressure and a materially smaller startup justify health rotation even
|
|
84
|
+
# without a price/overhead forecast. Capacity is host-observed, never guessed.
|
|
85
|
+
capacity = number("context_capacity_tokens")
|
|
86
|
+
occupancy = current / capacity if current is not None and capacity else None
|
|
87
|
+
pressure = yes("context_pressure") or (occupancy is not None and occupancy >= 0.70)
|
|
88
|
+
smaller_start = current is not None and current > 0 and fresh is not None and fresh <= 0.80 * current
|
|
89
|
+
health_benefit = pressure and smaller_start
|
|
90
|
+
assessment.update({
|
|
91
|
+
"context_occupancy": occupancy,
|
|
92
|
+
"context_pressure": pressure,
|
|
93
|
+
"materially_smaller_start": smaller_start,
|
|
94
|
+
"health_rotation_qualified": health_benefit,
|
|
95
|
+
})
|
|
96
|
+
triggered = yes("explicit_restart") or pressure or pays_back
|
|
97
|
+
compactions = number("compactions")
|
|
98
|
+
triggered = triggered or (compactions is not None and compactions >= 2)
|
|
99
|
+
age = number("session_age_seconds")
|
|
100
|
+
triggered = triggered or (state["mode"] == "daily" and age is not None and age >= 86400)
|
|
101
|
+
if creation_status == "not_sent" and not triggered:
|
|
102
|
+
return result("CONTINUE", "no_observed_trigger")
|
|
103
|
+
if not yes("safe_boundary") or number("active_writers") != 0 or number("pending_operations") != 0:
|
|
104
|
+
return result("WAIT_SAFE_BOUNDARY", "writes_or_operations_not_released")
|
|
105
|
+
if not yes("checkpoint_readback") or not yes("checkpoint_current"):
|
|
106
|
+
return result("CHECKPOINT", "persist_and_verify_current_state")
|
|
107
|
+
if creation_status == "not_sent":
|
|
108
|
+
if not yes("explicit_restart") and not pays_back and not health_benefit:
|
|
109
|
+
return result("CHECKPOINT_ONLY" if not measured else "CONTINUE", "benefit_unmeasured" if not measured else "restart_does_not_pay_back")
|
|
110
|
+
if not yes("explicit_restart") and not yes("progress_since_restart"):
|
|
111
|
+
return result("CONTINUE", "prevent_restart_without_progress")
|
|
112
|
+
if not yes("transfer_supported"):
|
|
113
|
+
return result("MANUAL_HANDOFF", "no_authoritative_ownership_transfer")
|
|
114
|
+
if not yes("fresh_create_supported") or not yes("target_read_supported"):
|
|
115
|
+
return result("MANUAL_HANDOFF", "native_transport_unavailable")
|
|
116
|
+
return result("CREATE_FRESH", "record_identity_and_receipt_before_retry")
|
|
117
|
+
if not yes("ack_matches"):
|
|
118
|
+
return result("VERIFY_TARGET", "exact_goal_checkpoint_and_target_ack_required")
|
|
119
|
+
if not yes("transfer_supported"):
|
|
120
|
+
return result("MANUAL_HANDOFF", "no_authoritative_ownership_transfer")
|
|
121
|
+
return result("COMMIT_TRANSFER", "conditional_transfer_required_target_stays_readonly")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> int:
|
|
125
|
+
try:
|
|
126
|
+
raw = sys.stdin.buffer.read(65537)
|
|
127
|
+
if len(raw) > 65536:
|
|
128
|
+
raise ValueError("input_too_large")
|
|
129
|
+
state = json.loads(raw)
|
|
130
|
+
if not isinstance(state, dict):
|
|
131
|
+
raise ValueError("expected_object")
|
|
132
|
+
output = decide(state)
|
|
133
|
+
except (ValueError, UnicodeDecodeError):
|
|
134
|
+
output = {"action": "BLOCKED", "reason": "invalid_input"}
|
|
135
|
+
print(json.dumps(output, sort_keys=True))
|
|
136
|
+
return 2 if output["action"] == "BLOCKED" else 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
raise SystemExit(main())
|