Skip to content

feat(photos): add upload and app-created album commands - #1185

Open
beyondzero wants to merge 1 commit into
openclaw:mainfrom
beyondzero:feat/photos-upload
Open

beyondzero wants to merge 1 commit into
openclaw:mainfrom
beyondzero:feat/photos-upload

Conversation

@beyondzero

Copy link
Copy Markdown

What

Adds the Photos Library API writes that still exist after Google's 2025 scope changes:

  • gog photos upload <file>... uploads bytes (uploads), then creates media items (mediaItems:batchCreate). It can add them to an app-created album (--album) and set a description (--description), and it reports a result per item. --dry-run is supported.
  • gog photos albums list and gog photos albums create <title> work on app-created albums.

Scope: upload is opt-in

The default photos scope is unchanged: photoslibrary.readonly.appcreateddata only. Uploading needs gog auth add <email> --services photos --photos-scope=append, which adds photoslibrary.appendonly. This follows the --gmail-scope / --drive-scope pattern, so existing read-only setups and --enable-commands photos allowlists gain no write access by default. --readonly with --photos-scope=append is refused at auth add. The existing --readonly transport already blocks POST /v1/uploads, mediaItems:batchCreate and POST /v1/albums; this was verified against a live token. When Google refuses an upload with 403, the error names the flag to re-run with.

File types: only photo and video extensions are accepted. Anything else (a text file, a key file) is refused locally, before any bytes are sent to the upload endpoint.

Regenerated: the README auth table (scripts/gen-auth-services-md.go), the command docs and the gog-photos agent skill (make agent-skills).

Why

Since the 2025 Library API changes, the only thing an app can do with a user's Photos library is create media and albums, and work inside what it created. gog photos could read app-created media but not create any. That left gog unable to do the one write the API still allows.

Testing

  • make ci passes (fmt-check, golangci-lint, deadcode, test, docs-check, agent-skills-check), with new tests in internal/cmd/photos_write_test.go and internal/googleauth/service_test.go (default stays read-only, append is opt-in, readonly+append and unknown modes are refused).
  • Used for real: about 1,200 uploads into an app-created album, with descriptions, from a scripted pipeline. Uploading the same bytes again returns the existing media item. That is Google's behaviour, and the per-item results report it as such.
  • Worth knowing: the description set through the API shows under Info → Other in Google Photos and is searchable. It is not the user-editable "Add a description" caption, which only a person in the app can set. This is documented in the command help.

🤖 Generated with Claude Code

@beyondzero
beyondzero requested a review from a team as a code owner October 2, 2026 02:42
@clawsweeper

clawsweeper Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Oct 2, 2026
@clawsweeper

clawsweeper Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed October 4, 2026, 12:23 PM ET / 16:23 UTC (Revision 3).

ClawSweeper review

What this changes

Adds Google Photos uploads and app-created album commands, an opt-in OAuth write scope, documentation, and regression tests.

Merge readiness

✅ Ready for maintainer review

This PR remains useful: current main and v0.43.0 lack the requested commands. The earlier findings are resolved, the live proof supports the authorization boundary, and no blocking patch defect remains.

Priority: P2
Reviewed head: 23b7f7dd3520c8763f8d702fbfbea2d7cd9b7a46

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) A focused implementation with convincing live authorization proof and resolved prior findings.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): Redacted live terminal output exercises the new Photos CLI and API client against a real Google account: append-authorized creation and upload succeed, writes fail after narrowing an existing grant, reads continue, and explicit re-widening restores uploads. Automatic reauthorization has supplemental regression coverage rather than a live revocation run. No stored-data format changes require migration.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): Redacted live terminal output exercises the new Photos CLI and API client against a real Google account: append-authorized creation and upload succeed, writes fail after narrowing an existing grant, reads continue, and explicit re-widening restores uploads. Automatic reauthorization has supplemental regression coverage rather than a live revocation run. No stored-data format changes require migration.
Evidence reviewed 8 items Pinned change ownership: Inspected the merge-base-to-head delta across all 22 files. The original PR head and actual checkout are 23b7f7d; main is its recorded parent. No base-only changes are attributed to this PR.
Still necessary on main and release: Main's Photos command tree exposes list, search, get, download, and Picker, but no upload or album commands. Inspection of v0.43.0 shows the same command tree; the PR is not already implemented.
Write access remains explicit: Default Photos authorization still requests only read-only app-created access. Append mode adds appendonly, and readonly plus append is rejected. No stored token schema or automatic scope migration is introduced.
Findings None None.
Security None None.

How this fits together

The Photos CLI connects account credentials and local media files to Google's Photos Library API. It returns app-created media and album information as JSON or terminal output.

flowchart TD
  A[Local media files] --> B[Photos commands]
  C[Account and OAuth scopes] --> D[Authenticated client]
  B --> D
  D --> E[Read-only request guard]
  E --> F[Google Photos API]
  F --> G[Media and album results]
  G --> B
Loading

Before merge

None.

Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production and test growth production +601 net lines; tests +273 lines Production growth implements the stated upload, album, and authorization capability; generated documentation accounts for additional diff size.

Technical review

Best possible solution:

Extend the existing Photos client with explicit opt-in writes while preserving read-only defaults and narrowed grants.

Do we have a high-confidence way to reproduce the issue?

Not applicable as a bug reproduction: this adds commands absent from main. The contributor supplies a concrete real-account transcript demonstrating the new behavior.

Is this the best way to solve the issue?

Yes. Reusing the existing Photos client and scope-selection pattern is a focused implementation, and the updated narrowing logic preserves the intended authorization boundary.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 414e2ff8afa2.

Labels

Label changes:

No label changes.

Label justifications:

  • P2: This is a bounded Photos capability addition with explicit write authorization and no demonstrated urgent regression.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): Redacted live terminal output exercises the new Photos CLI and API client against a real Google account: append-authorized creation and upload succeed, writes fail after narrowing an existing grant, reads continue, and explicit re-widening restores uploads. Automatic reauthorization has supplemental regression coverage rather than a live revocation run. No stored-data format changes require migration.
  • proof: sufficient: Contributor real behavior proof is sufficient. Redacted live terminal output exercises the new Photos CLI and API client against a real Google account: append-authorized creation and upload succeed, writes fail after narrowing an existing grant, reads continue, and explicit re-widening restores uploads. Automatic reauthorization has supplemental regression coverage rather than a live revocation run. No stored-data format changes require migration.

Evidence

What I checked:

  • Pinned change ownership: Inspected the merge-base-to-head delta across all 22 files. The original PR head and actual checkout are 23b7f7d; main is its recorded parent. No base-only changes are attributed to this PR. (23b7f7dd3520)
  • Still necessary on main and release: Main's Photos command tree exposes list, search, get, download, and Picker, but no upload or album commands. Inspection of v0.43.0 shows the same command tree; the PR is not already implemented. (internal/cmd/photos.go:19, 414e2ff8afa2)
  • Write access remains explicit: Default Photos authorization still requests only read-only app-created access. Append mode adds appendonly, and readonly plus append is rejected. No stored token schema or automatic scope migration is introduced. (internal/googleauth/service.go:613, 23b7f7dd3520)
  • Prior authorization finding resolved: Auth add disables incremental grants when Photos is narrowed, and automatic reauthorization applies the same rule to stored read-only Photos scopes. The new Reauth regression exercises read-only, mixed-service, and append grants. These changes address the earlier grant-narrowing finding. (internal/googleauth/reauth.go:161, 23b7f7dd3520)
  • Real authorization and write proof: The captured contributor comment at feat(photos): add upload and app-created album commands #1185 (comment) contains redacted terminal output from one consumer account and Desktop OAuth client: album creation and upload succeed with append access; narrowing the existing grant causes upload rejection and album-create rejection while album listing continues; explicitly restoring append access restores upload success. REST inspection confirmed the supplied comment. Automatic invalid_grant recovery was not exercised live and is covered by the focused Reauth regression instead.
  • Runtime read-only boundary: The existing outer read-only transport rejects the new upload, batch-create, and album-create POSTs before forwarding them. Photos search remains the only permitted Photos POST. (internal/googleapi/read_only.go:105, 23b7f7dd3520)

Likely related people:

  • Peter Steinberger: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • higginz777: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (2 earlier review cycles)
  • reviewed 2026-10-02T02:45:54.448Z sha 5c40088 :: needs real behavior proof before merge. :: [P1] Disable incremental grants when narrowing Photos access | [P3] Add the required changelog reference and contributor credit
  • reviewed 2026-10-04T15:40:21.731Z sha 23b7f7d :: needs maintainer review before merge. :: none

Photos Library API writes that remain after the 2025 scope changes:
'photos upload' (raw upload + mediaItems:batchCreate, optional album and
description, per-item results, --dry-run) and 'photos albums list|create'.

Upload is opt-in: 'gog auth add --photos-scope=append' adds
photoslibrary.appendonly; the default photos scope stays read-only, and
--readonly with --photos-scope=append is refused. Only photo and video
file types are sent, so an arbitrary file's bytes never reach the upload
endpoint. The --readonly transport already blocks the new POSTs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@beyondzero

Copy link
Copy Markdown
Author

Thanks for the review. This addresses all three items: the P1 grant narrowing, the changelog, and the real-behaviour proof.

Fix: narrowing Photos no longer keeps an earlier append grant

  • auth add: when photos is requested without --photos-scope=append, include_granted_scopes is disabled (photosNarrowsGrant).
  • Automatic reauthorization (googleauth.Reauth): a stored Photos grant that has the read scope but no appendonly is treated as narrowed, using the same mechanism as hasLimitedGmailGrant (hasLimitedPhotosGrant).
  • Tests:
    • TestPhotosNarrowsGrant and TestHasLimitedPhotosGrant.
    • TestReauthKeepsNarrowedPhotosGrantNarrow, which drives the real Reauth() with read-only, read-only + calendar, and append stored grants. Without the reauth.go change it fails (disable incremental grants = false, want true) for both narrowed cases; with the change it passes.
  • Changelog: the entry now cites (#1185) — thanks @beyondzero.
  • Error hint: the --photos-scope=append hint now also covers the uploads endpoint's 401. Measured below: Google refuses uploads with 401 and the JSON endpoints with 403.
  • make ci passes: fmt-check, golangci-lint (0 issues), deadcode, test, docs-check, agent-skills-check.

Authority-chain proof (live)

This uses one real consumer Google account and one Desktop OAuth client throughout, with a generated test image and a throwaway app-created album. The account, client ID, album and media IDs, and local paths are redacted.

Generated authorization URLs (auth add --remote --step 1, nothing consented):

--photos-scope=readonly: include_granted_scopes=(absent)  scopes=[email, photoslibrary.readonly.appcreateddata, userinfo.email, openid]
--photos-scope=append:   include_granted_scopes=true      scopes=[email, photoslibrary.appendonly, photoslibrary.readonly.appcreateddata, userinfo.email, openid]

A. Append grant: album create and upload succeed. The stored scopes include photoslibrary.appendonly.

$ gog photos albums create "gogcli PR 1185 proof"
{"album": {"id": "<album-id>", "title": "gogcli PR 1185 proof", "isWriteable": true, ...}}
$ gog photos upload proof-1.jpg --album <album-id>
{"failed": 0, "results": [{"file": "proof-1.jpg", "mediaItemId": "<media-id>", "status": "ok"}], "uploaded": 1}

B. Narrow the same account and client to read-only (default Drive/Gmail modes):

$ gog auth add <account> --services photos --photos-scope=readonly --force-consent
  (authorization URL: scope=email photoslibrary.readonly.appcreateddata userinfo.email openid; no include_granted_scopes)
Authorization received. Finishing…
services  photos
recorded scopes: [email, photoslibrary.readonly.appcreateddata, userinfo.email, openid]

C. Writes after narrowing, without --readonly, so Google decides:

$ gog photos upload proof-2.jpg --album <album-id>
error   proof-2.jpg   photos API error (401): {"code": 16, "message": "Authentication session is not defined."}
uploaded 0 of 1
exit 1
$ gog photos albums create "gogcli PR 1185 proof (should fail)"
photos API error (403 PERMISSION_DENIED): Request had insufficient authentication scopes.
exit 6
$ gog photos albums list        # read access is unaffected
<the proof album is listed>

D. Re-widening needs the explicit opt-in, and then works again:

$ gog auth add <account> --services gmail,photos --gmail-scope readonly --photos-scope=append --force-consent
recorded scopes: [email, gmail.readonly, photoslibrary.appendonly, photoslibrary.readonly.appcreateddata, userinfo.email, openid]
$ gog photos upload proof-3.jpg --album <album-id>
ok   proof-3.jpg   <media-id>
uploaded 1 of 1

Automatic reauthorization runs only after invalid_grant and stops for an interactive confirm. Triggering it live would have meant revoking the app's access for the whole account, which would also revoke other tokens on this client. It is covered instead by TestReauthKeepsNarrowedPhotosGrantNarrow above, which exercises the real Reauth() path and fails without the fix.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Oct 4, 2026
@beyondzero

Copy link
Copy Markdown
Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
Exact review queued.

Re-review progress:

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant