Commit graph

11 commits

Author SHA1 Message Date
Alireza Rezvani
8b0fca4a4d
fix(book-to-skill): correct emitter docstring drift and the resident-core figure
Addresses both review findings on #993.

1. skill_plugin_emitter.py's module docstring still described the behaviour this
   PR removed. Its tree diagram called plugin.json the "manifest with
   ./skills/<slug> + attribution", and the rights-gate paragraph said
   cleared_for_distribution is recorded "in the manifest" -- both now the
   sidecar. A docstring asserting the opposite of its own file's point is worse
   than no docstring, and it is what `--help` readers see.

   Also corrects a third instance the review did not name but which shares the
   root cause: the tree diagram never listed authoring-notes.json at all, even
   though this PR's fix is what creates it. Added, with the issue #954 reason
   stated inline so the next reader knows why provenance cannot live in the
   manifest.

2. CLAUDE.md said the resident core is 2,066 tokens while, twelve lines later,
   the same file said 2,101. 2,066 predates the worked invocation block added
   for write-a-skill checklist item 5; token_budget_estimator.py reports 2,101
   on the current tree, matching CHANGELOG.md and the PR body. CLAUDE.md was
   the only stale copy and it contradicted itself.

Docstring-only and prose-only; no behaviour change. Verified the emitter still
emits both plugin.json and authoring-notes.json via --sample after the edit.

Gates: compileall, check_plugin_json --all, check_paths, check_frontmatter,
check_dual_publish, check_model_freshness, smoke_scripts (696/696),
derive_counters --check, book_skill_validator --strict.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UySnyf5upm4y8xhYA3w6yw
2026-08-25 22:57:41 +00:00
Alireza Rezvani
40fa75258a
docs(book-to-skill): add attribution block to the compiled skill's sidecar
Addresses the review observation on #993: the compiled skill's
authoring-notes.json carried only a `source` block (how it was built) even
though its content is derived from an external MIT-licensed work, where the
rest of the repo uses an `attribution` block for that. check_plugin_json.py's
NOTES_ALLOWED permits both keys, so the two coexist.

Adds `attribution` to engineering/spinning-up-deep-rl following the shape used
by book-to-skill and skillopt-sleep: derived_from, upstream_docs, upstream_path,
original_author, original_license, original_copyright, derivation_note.

The emitter is deliberately NOT changed to synthesise this. It knows only
`--source-note` free text and a rights basis -- not an upstream URL, author or
licence -- and a half-filled attribution block is worse than none. Instead
Step 11 of conversion_workflow.md now says attribution is added by hand
whenever `--rights` is anything but internal-docs, names the field shape, and
restates that the actual obligation is the LICENSE notice and README credit --
authoring-notes.json is metadata Claude Code never reads, and a sidecar JSON
file is not a licence notice.

Gates re-run clean: check_plugin_json --all, check_paths, check_frontmatter,
check_dual_publish, check_model_freshness, smoke_scripts (692/692),
derive_counters --check, book_skill_validator --strict.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UySnyf5upm4y8xhYA3w6yw
2026-08-25 18:57:31 +00:00
Alireza Rezvani
800a0d5672
feat(engineering): compile OpenAI's Spinning Up in Deep RL into a knowledge-base plugin
Runs engineering/book-to-skill end to end on its first real source: OpenAI's
Spinning Up in Deep RL (MIT, (c) 2018 OpenAI; primarily developed by Joshua
Achiam). Cloned openai/spinningup and compiled its docs/ reStructuredText tree
(38 files, ~37k words, ~49K tokens) through the full pipeline -- extract
--mode technical, analysis, 20 chapter files, glossary/patterns/cheatsheet,
master SKILL.md, validator, plugin emitter.

The compiled skill passes book_skill_validator.py in --strict mode with every
file inside budget: a 2,101-token resident core (cap 4,000) plus 20 on-demand
chapters averaging ~1,256 tokens each.

Chapter structure follows the source's own toctree rather than a heading scan:
user documentation (ch01-06), Introduction to RL Parts 1-3 (ch07-09), the
researcher essay / key papers / exercises / benchmarks (ch10-13), one chapter
per algorithm in lineage order (ch14-19: VPG to TRPO to PPO, DDPG to TD3 and
SAC), and the logger/MPI/ExperimentGrid utilities (ch20).

Rights basis is open-license, not fair use -- the emitter's Step-11 gate
refuses a shareable package without one. Upstream's MIT notice is reproduced
in full in the plugin's LICENSE beside this package's own, and README.md names
the source, the author and the source's frozen version; a sidecar JSON is not
a license notice.

Also fixes a defect the emitter only reveals at its final step:
skill_plugin_emitter.py wrote its whole `source` provenance block into
plugin.json, on a stale inline claim that `source`/`attribution` were approved
extension fields. Claude Code rejects an entire manifest on any unrecognized
key (issue #954) and scripts/check_plugin_json.py hard-fails such a manifest,
so every package the emitter produced failed the blocking CI gate on commit.
_plugin_manifest() now emits spec fields only and a new _authoring_notes()
writes .claude-plugin/authoring-notes.json. Recorded as deviation 26 in
engineering/book-to-skill/README.md; the printed marketplace.json snippet is
unchanged, since `source` is a valid key there.

Counters: skills 386 -> 387, agents 116 -> 117, commands 146 -> 147, plugins
97 -> 98. Tools and references unchanged -- a compiled knowledge base ships
notes, not scripts.

All blocking CI gates verified locally: compileall, check_plugin_json --all,
check_skill_names, check_paths, check_frontmatter, check_dual_publish,
check_model_freshness, smoke_scripts (692/692), derive_counters --check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UySnyf5upm4y8xhYA3w6yw
2026-08-25 18:49:30 +00:00
Claude
7405298b4b
fix: resolve the actionable reported issues (#954, #949, #933, #931, #969, #968, #924, #885)
- #954: strip non-spec source/attribution keys from all 39 plugin.json
  manifests so Claude Code's validator accepts them; metadata preserved in
  new .claude-plugin/authoring-notes.json sidecars; check_plugin_json.py now
  hard-fails manifests carrying those keys and sanity-checks the sidecar;
  CLAUDE.md ClawHub schema section updated to the new rule.
- #949: move the c-level-agents plugin out of c-level-advisor/ to a
  top-level directory so the two marketplace sources no longer overlap;
  updated marketplace.json source, homepage, descriptions, all
  cross-references, docs, harness manifest, mirror-tree symlinks/indexes,
  and rebased the moved files' relative links; domain counters trued up
  (18 -> 19 domains).
- #933: replace dead links to the gitignored maintainer-local megaprompts/
  tree with annotated plain-text references (44 files: SKILL.md, READMEs,
  agents, commands).
- #931: DynamoDB on-demand pricing updated to post-Nov-2024 rates
  ($0.625/M writes, $0.125/M strongly consistent reads).
- #969: skill_security_auditor.py and the three dossier scripts reconfigure
  stdout/stderr to UTF-8 (errors=replace) so legacy Windows codepages no
  longer crash at print time; PYTHONUTF8=1 documented.
- #968: Windows Notes section in INSTALLATION.md + README pointer for the
  core.symlinks mirror-tree checkout caveat.
- #924/#885 residuals: hook commands quote "${CLAUDE_PLUGIN_ROOT}" paths in
  all plugin hooks.json/settings.json (space-safe roots); removed the stale
  pre-rename status/review mirror symlinks and index entries left over from
  the memory-status/memory-review rename.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qgc6RYXWJPr5oW9DHU7zR4
2026-08-21 05:47:37 +00:00
Claude
3fa59a566f
fix(book-to-skill): close the workdir race with fd pinning
Addresses the residual TOCTOU raised in the fourth review. The reviewer called
it non-blocking; it verified as slightly worse than described, and the fix is
small, so it is closed rather than deferred.

The claim checks out: `mkdir(parents=True, exist_ok=True)` does NOT raise on a
symlink-to-directory, because its exists-branch tests `is_dir()`, which follows
symlinks. Demonstrated directly — mkdir succeeded silently on a planted link and
a subsequent write landed in the attacker's directory.

What the review did not note is why the second layer failed to catch it: a file
inside a swapped directory is an ordinary file, not a symlink, so
`_write_private`'s `is_symlink()` check could never see a directory swap. The
artifact-level guard did not back up the directory-level one at all.

Three changes:

- `resolve_workdir()` attempts `mkdir` FIRST and only inspects a path that
  already existed, via `os.lstat` — which does not follow the final component.
  That removes the check-then-create ordering.
- `open_workdir()` pins the directory with `O_NOFOLLOW|O_DIRECTORY`, and both
  artifacts are written through that descriptor. An fd names an inode, so a
  rename or symlink swap of the path afterwards cannot redirect the write.
- `_write_private()` creates with `O_CREAT|O_EXCL|O_NOFOLLOW` at mode 0600 —
  no check-then-act window at all. An artifact from a previous run into the same
  --workdir is unlinked first; unlink removes the link, never its target.

Verified against a live race: pin the directory, rename it away, plant a symlink
to an attacker directory, then write — data lands in the pinned inode, attacker
directory stays empty. Also verified a pre-planted `full_text.txt -> victim`
symlink leaves the victim's content intact and is replaced by a 0600 file we own,
and that re-running into the same --workdir still succeeds.

Degrades to the previous path-based checks where `dir_fd`/`O_NOFOLLOW` are
unavailable (Windows).

Recorded as deviation 25. Full regression re-run: EPUB bomb, EPUB entity,
extensionless sniff bomb, DOCX bomb, emitter symlink, rights gate and the
estimator path check all still refuse; a clean EPUB still extracts. All gates
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zu9Gmm9S78c2t3kDLnpPX
2026-08-05 08:17:16 +00:00
Claude
b6a1687ce3
fix(book-to-skill): skill-quality audit — runnable docs, honest gates
Read the skill as a skill rather than as code, which the previous three review
rounds had not done. Four findings, all now fixed and verified.

1. The documented quick-start did not run. SKILL.md's copy-paste block referenced
   $WORKDIR and $SKILLS_HOME without ever assigning them, so following it
   literally produced a FileNotFoundError traceback at step 2. Both are now real
   assignments, and all five steps were executed verbatim end to end as a check.
   The plugin README's block had the same defect and is fixed the same way. A
   quick-start that does not run is the worst kind of doc bug: it is the part a
   reader trusts most.

2. A gate tool reported success for a path that was not there.
   `token_budget_estimator.py --skill-dir <typo>` produced a complete,
   plausible-looking budget audit — every row "missing", every cap satisfied,
   exit 0 — which reads as a pass. It now refuses a missing directory, a
   non-directory, and a directory with no SKILL.md (exit 2). `--full-text
   <missing>` raised a bare traceback and now refuses cleanly. The other three
   tools already validated their inputs; this one was the outlier.

3. Three upstream artifacts cleaned, one of them load-bearing. epub.py's
   `except (KeyError, Exception)` is simply `except Exception` — it swallowed
   everything including the size refusal `safe_read()` now raises, quietly
   disarming deviation 17 at that call site. Narrowed so ExtractionError
   propagates and only genuine parse failures fall through to the .opf glob.
   utils.py emitted a dynamic {pages_label: pages} key beside a literal "pages",
   colliding whenever the label was "pages"; the alias is now conditional. A
   stray artifact word removed from a pdf.py comment.

4. `tool | head` no longer tracebacks. Observed once on the emitter (racy on
   flush timing, 0/20 on retry) — all four CLIs now exit 141 quietly, the
   standard SIGPIPE convention.

Token cost re-measured: SKILL.md 2,256 tokens resident (229 lines), references
10,216 on demand. Healthy against the ~5k practical ceiling for a resident body.

Full security regression re-run after the changes: EPUB bomb, EPUB entity,
extensionless sniff bomb, DOCX bomb, planted workdir symlink, emitter symlink
and the rights gate all still refuse; a clean EPUB still extracts.

Recorded as deviations 21-24; count synced across plugin.json, CLAUDE.md and
CHANGELOG. All gates green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zu9Gmm9S78c2t3kDLnpPX
2026-08-05 08:07:42 +00:00
Claude
058447e4a6
fix(book-to-skill): route the sniff path through the zip budget; emitter fixes
Addresses the third automated review on PR #941.

[High] The magic-byte sniffing path bypassed the zip-bomb budget it was built
to enforce. extract_single_file() reads a `mimetype` member with a bare
zf.read() when the extension is unrecognized — the earliest attacker-controlled
point in the pipeline, running before a format is chosen and before any check in
zip_safety.py. zip_safety.py's own docstring claims "every read goes through
safe_read()"; this one did not, which makes it a documentation defect as much as
a security one. Now routed through safe_read(). Its ExtractionError sits outside
the surrounding except tuple on purpose, so a bomb reports as a bomb rather than
as a generic unsupported format.
Verified: a 200 MB / 1029x fixture with no file extension is refused at ~15 MB
peak RSS instead of being decompressed.

[Medium] --author / --author-url never reached the printed marketplace entry.
_plugin_manifest() threaded them correctly into the emitted plugin.json, but
_marketplace_entry() took no author parameter and hardcoded one name — so the
snippet whose entire purpose is preventing hand-edit mistakes contradicted the
manifest sitting next to it for anyone but the default author. Threaded through.
Verified: --author "Jane Doe" now appears in both.

[Low] Narrow TOCTOU between _assert_no_symlinks() and copytree. copytree already
runs with symlinks=True, so a link planted in that window is copied as a link
rather than dereferenced — no content leak. Now fully closed: the emitted tree
is re-walked after the copy, and the package is deleted rather than shipped if
any link appeared.
Verified with a monkeypatched guard that plants a symlink immediately after the
check passes: refused, package removed, secret content absent.

[Nit] plugin.json asserted "license": "MIT" unconditionally, with the "MIT
covers the converter, not the compiled content" caveat living only in README
prose. Added source.license_scope stating it in the manifest, so a tool reading
only the manifest sees the distinction, plus a code comment at the assignment.

Recorded as deviations 19 and 20; count synced across plugin.json, CLAUDE.md
and CHANGELOG.

All gates green: compileall, check_paths --all, check_dual_publish,
smoke_scripts (0 failed), derive_counters --check, check_plugin_json --all
(0 FAIL). All four CLIs pass --help / --sample.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zu9Gmm9S78c2t3kDLnpPX
2026-08-05 07:45:03 +00:00
Claude
9ed709aef3
fix(book-to-skill): guard EPUB XML, cap zip expansion, refuse symlinked trees
Addresses the second automated review on PR #941. All three code findings
verified against the actual behaviour, not just patched.

[Medium] shutil.copytree dereferenced symlinks the validator never saw. The
validator checks SKILL.md, the three supporting files and chapters/*.md; the
copy then followed a link anywhere else in the tree (assets/, any subdirectory)
and baked the target's real content into a package that can go out as
--distribution shareable. _assert_no_symlinks() now walks the whole tree and
refuses, and runs BEFORE the validation branch so --skip-validation cannot
bypass it. copytree also passes symlinks=True so loosening that check later
cannot silently reintroduce dereferencing.
Verified: a symlink in assets/ pointing at a secret file is refused both with
and without --skip-validation, the secret never lands in a package, and a clean
tree still emits.

[Medium] The DOCX XXE/entity guard did not extend to EPUB's ebooklib path.
Upstream hardened DOCX only. EPUB is the same zip-of-XML shape and ebooklib —
one of the packages this skill recommends installing — parsed container.xml,
the OPF and content docs with no equivalent pre-check. The guard moved to a new
book_to_skill/zip_safety.py and now runs for both formats.
Verified: an EPUB whose OPF declares an entity is refused; a clean EPUB still
extracts and detects its chapter.

[Low] No size cap before decompressing zip members. Every archive read now goes
through safe_read(), which checks the declared uncompressed size and the
compression ratio against the central directory before decompressing, and
charges actual bytes against a per-archive budget so a lying directory cannot
get past it either.
Verified: a 200 MB / 1029x bomb is refused at ~14 MB peak RSS instead of being
materialized.

[Low] The PR body's "12 numbered items" was stale against README's list. Fixed
in the PR description; the in-repo count is synced to 18 across plugin.json,
CLAUDE.md and CHANGELOG.

Recorded as deviations 17 and 18. Counters: tools 662 -> 663 (zip_safety.py);
that module is allowlisted in smoke_exceptions.txt like its siblings.

All gates green: compileall, check_paths --all, check_dual_publish,
smoke_scripts (0 failed), derive_counters --check, check_plugin_json --all
(0 FAIL). Security auditor unchanged at 0 critical / 4 high.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zu9Gmm9S78c2t3kDLnpPX
2026-08-05 07:31:04 +00:00
Claude
b5031e976e
fix(book-to-skill): private per-invocation workdir; share budget constants
Addresses the automated review on PR #941.

Security (the one item flagged as wanted-before-merge): upstream defaults the
extraction workdir to a fixed `<tempdir>/book_skill_work`. On a shared host that
is CWE-377/CWE-59 — any local user can pre-create the directory in a
world-writable /tmp (the sticky bit prevents deletion, not creation) and plant a
symlink named full_text.txt or metadata.json pointing at a file the victim can
write, because Path.write_text follows symlinks. Two concurrent runs also
silently clobber each other.

- Default workdir is now a fresh `tempfile.mkdtemp(prefix="book_skill_work_")`:
  unpredictable name, 0700 by construction, never shared with a concurrent run.
  The path is printed and carried in metadata.json's `output_text`.
- Artifacts are written 0600, and each write refuses a symlink at the target.
- An explicit --workdir / BOOK_SKILL_WORKDIR is still honoured, but is
  symlink-refused, created 0700, and chmod-tightened if it already exists.
- parsers/calibre.py no longer writes its ebook-convert scratch file to the
  shared directory. That also fixes a real bug the review did not name: it read
  a module-level OUTPUT_DIR constant, so the scratch file ignored --workdir
  entirely and escaped the directory the caller asked for.

Verified: default workdir 0700 with 0600 artifacts and a per-invocation name;
two runs get distinct directories; a 777 --workdir is tightened to 700; a
symlinked workdir is refused; and a planted `full_text.txt -> victim` symlink is
refused with the victim file left untouched.

Also from the review:
- book_skill_validator.py and token_budget_estimator.py restated the same
  BUDGETS dict. Both now import SKILL_FILE_BUDGETS / CHAPTER_TOKEN_CEILING from
  book_to_skill/config.py so the two gating tools cannot drift.
- Corrected the smoke_exceptions.txt rationale: the list is "modules the G8
  probe trips on", not "modules that aren't CLIs". config.py, exceptions.py,
  sanitize.py, parsers/__init__.py, parsers/pdf.py and parsers/text.py are
  equally not CLIs and pass only because they have no argv handling.

Recorded as deviations 15 and 16; count synced in plugin.json, CLAUDE.md and
CHANGELOG. Docs updated: the workdir path is now read from the tool's output
rather than hardcoded.

All gates green: compileall, check_paths --all, check_dual_publish,
smoke_scripts (0 failed), derive_counters --check, check_plugin_json --all
(0 FAIL). Security auditor unchanged at 0 critical / 4 high (documented).
End-to-end pipeline re-run clean: extract -> verdict -> validate -> emit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zu9Gmm9S78c2t3kDLnpPX
2026-08-05 07:14:57 +00:00
Claude
4f5a6825f2
fix(book-to-skill): satisfy CI path, smoke, and stdlib-shadowing gates
CI gate G1 (check_paths.py) failed: the agent's tool table referenced
`scripts/<tool>.py`, which resolves relative to the agent's own folder, not the
skill's. Now uses `../skills/book-to-skill/scripts/...` like cs-skill-author.

Two more issues found running the full gate set locally:

- Renamed `parsers/html.py` -> `parsers/html_text.py`. A module named `html.py`
  shadows the stdlib `html` package whenever its own directory lands on
  sys.path[0], and `import html.parser` then fails with "'html' is not a
  package". Renaming removes the hazard rather than documenting it; two import
  lines changed. Verified: HTML extraction still detects chapters, emits block
  boundaries, and tab-joins table cells.
- Registered the eight vendored library modules in scripts/smoke_exceptions.txt.
  They are imported as `book_to_skill.*`, never run as CLIs, so gate G8's
  `--help` probe can only ever fail on them. The four real entry points are
  smoke-tested normally and pass.

Recorded as deviations 13 and 14 in the plugin README; count synced in
plugin.json, CLAUDE.md and CHANGELOG.

All blocking gates green locally: compileall, check_plugin_json --all (89 OK),
check_paths --all (0 findings), check_dual_publish (0 drift), smoke_scripts
(0 failed), derive_counters --check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zu9Gmm9S78c2t3kDLnpPX
2026-08-05 07:05:54 +00:00
Claude
b19c40cf95
feat(engineering): add book-to-skill — compile documents into knowledge-base skills
Derived from virgiliojr94/book-to-skill (MIT). Compiles a book, docs folder, or
spec collection (PDF, EPUB, DOCX, HTML, Markdown, RST, AsciiDoc, RTF, MOBI/AZW)
into an agent skill: a resident master SKILL.md (core frameworks + chapter index
+ topic index, capped at 4k tokens) plus on-demand chapter files, a glossary, a
patterns file, and a decision cheatsheet.

The extraction library (scripts/book_to_skill/, 12 modules incl. 7 per-format
parsers) is vendored close to verbatim and keeps upstream's format chains,
chapter detection across Latin/Roman/Chinese/Thai/Korean heading styles,
invisible-Unicode (Trojan Source) sanitization, and the DOCX entity guard.

12 numbered deviations recorded in the plugin README (authoritative list):

- No implicit installs: --install-missing defaults to `report`, printing the pip
  command and using the stdlib fallback, where upstream prompts on a TTY and
  installs into the caller's environment.
- Rights gate: emitting a shareable package refuses without --rights from
  public-domain|open-license|internal-docs|author-permission. `fair-use` is
  deliberately excluded — a defence, not a licence.
- Validator merged and extended: upstream's two validators become one four-family
  gate, adding budget (token caps) and index (dead chapter links, unindexed
  chapters, dangling topic refs) — the failure that silently breaks navigation
  while the skill still looks complete.
- Folded YAML scalars now parse, so a wrapped description no longer under-reports
  its length past the 1024-char cap.
- token_budget_estimator replaces discovery_tax: tiktoken path dropped for one
  deterministic estimator, post-flight budget audit added, plus an explicit
  worth-converting verdict that says "just read it" below ~3x the compiled size.
- Two PRIV-ESC criticals fixed: upstream install hints contained a literal
  `sudo apt install`; they now name the package manager without escalating.

Repo-native addition with no upstream counterpart — Step 11 / /cs:book-to-plugin:
upstream stops at a bare ~/.claude/skills folder this library cannot route to.
skill_plugin_emitter.py wraps a compiled skill as a full plugin package (manifest
+ cs-<slug> agent + /cs:<slug> command + README) and prints the marketplace entry
without editing marketplace.json. Its --force path is guarded against symlinks,
paths outside the destination root, and non-package directories.

Ships 4 stdlib-only tools (all --help/--sample/--output json), 5 references citing
7-8 sources each, 3 asset templates, cs-book-to-skill agent, 2 commands.
Cross-linked into write-a-skill ("author first, compile second").

Regenerated the engineering harness manifest: picked up book-to-skill plus three
skills that had drifted out (minimalist, skillopt-sleep, strict-api), 81 -> 85.

Counters: skills 362 -> 363, tools 644 -> 662, refs 741 -> 746, agents 102 -> 103,
commands 116 -> 118, plugins 88 -> 89 (derive_counters.py --check passes).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zu9Gmm9S78c2t3kDLnpPX
2026-08-05 07:01:23 +00:00