Skip to content

Implement local Apify actor runtime (POCv2) - #11

Merged
Pijukatel merged 19 commits into
v2-poc-requirementsfrom
claude/shepherd-local-actor-runtime-n0jcl0
Aug 18, 2026
Merged

Implement local Apify actor runtime (POCv2)#11
Pijukatel merged 19 commits into
v2-poc-requirementsfrom
claude/shepherd-local-actor-runtime-n0jcl0

Conversation

@Pijukatel

Copy link
Copy Markdown
Collaborator

What

The POCv2 local actor runtime described by requirements/*.md: a TypeScript service exposing an Apify-compatible REST API (port 3333) backed by Crawlee v4 storage frontends (@crawlee/core + @crawlee/fs-storage, exact-pinned 4.0.0-beta.133), a dockerode-based actor build/run driver, a server-rendered view-only console (port 3000), TS + Python sample actors that take a maxPages input, and a CLI-only e2e suite. Driven by stock apify-cli 1.8.0 via APIFY_CLIENT_BASE_URL.

Why

The branch previously carried only the requirements draft and a sample actor stub — no implementation of the local apify push → build → run → inspect loop.

Notes

  • The requirements files were updated alongside the implementation; storage.md gains a "Known differences from the Apify platform" section (no request-queue locking, best-effort request listing, request deletion answers 501), per the agreed simplifications.
  • Verified by 196 unit/integration tests driven through a real apify-client, plus live checks with stock apify-cli (login, info, push mechanics, runs ls, datasets info). The e2e suite requires a Docker daemon and skips cleanly without one, so image build/run paths ran against an injectable driver rather than real containers in CI-less verification.
  • Storage access goes exclusively through Crawlee's Dataset/KeyValueStore/RequestQueue frontends with the service locator configured with purgeOnStart: false; the only backend-level call is teardown() at shutdown.

Follow-ups

  • Known nit: @types/js-yaml devDependency is redundant with js-yaml@5's bundled types.
  • The __FILES__ internal registry is currently unused (kept as designed; natural home for the TARBALL source-upload follow-up).
  • Deferred work tracked as issues: TARBALL source upload, persistent request index, frontend-instance eviction near the event-manager listener ceiling, apify actors start/abort/resurrect polish, DinD mode, read-only debug mount, Python sample lockfile parity.

🤖 Generated with Claude Code

https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE


Generated by Claude Code

- Add the TypeScript runtime: Apify-compatible REST API over Crawlee v4
  storage frontends, docker-based actor build/run driver, server-rendered
  console, and TS + Python sample actors with an input-driven e2e suite.
- Update requirements/*.md to match the delivered behavior, including the
  documented known differences from the real platform (no request-queue
  locking, best-effort request listing, unsupported request deletion).
- Verified by 196 unit/integration tests through a real apify-client, plus
  live checks with stock apify-cli 1.8.0; e2e is docker-gated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
claude and others added 18 commits August 15, 2026 10:59
- One job runs build, lint, format check, and the unit + integration
  suites; a second job runs the mandatory CLI-only e2e dev-loop suite
  against the runner's Docker daemon, per requirements/test.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
- Run responses now carry options.build, options.diskMbytes, generalAccess,
  and storage responses carry consoleUrl; build numbers start at .1 —
  validated against the Apify Python client's pydantic models, which
  reject the previous shapes.
- The driver waits for the container's demuxed log streams to end (not just
  container.wait) and logs are flushed before the terminal status is
  written, so a log fetched right after a run finishes is never empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
- Four integration tests drive the real apify-client log stream against
  the server across terminal-transition timing sweeps (the CI exit-13
  scenario could not be reproduced locally; these pin the settled-stream
  contract on every path).
- On e2e failure, CI now dumps the runtime container's logs so the
  server-side view of a failed request is visible in the job output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
- docker-modem's demuxStream never ends the destination streams, so
  awaiting their end held every real run open until the 300s timeout.
  Log drain is now derived from the source stream's end (destinations
  ended manually), raced against container exit plus a 5s grace.
- Test stubs now model demuxStream faithfully (data-forwarding only);
  a regression test finalizes a run in under a second against that stub.
- The e2e dumps the runtime container's logs before removing it, so CI
  failures show the server-side view (afterAll ran before the workflow's
  dump step, which always found the container already gone).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
apify-cli writes log content to stderr (stdout carries machine-readable
payloads), so the log-content assertion read an empty stdout while the
full log was present on stderr. The runtime side was already correct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
Actor SDK loggers emit ANSI SGR sequences; the console showed them as
literal [32m... garbage. Log views now convert SGR codes to colored
HTML spans (unsupported control sequences stripped, text HTML-escaped);
the /v2/logs API keeps serving raw bytes so CLI terminal colors work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
Both samples drop the in-container synthetic server and crawl the live
web through the Actor's request queue: startUrl input (default
https://crawlee.dev/) plus maxPages bounding the crawl, one {url,title}
dataset item per page. The e2e therefore needs outbound network from
Actor containers, documented in requirements/test.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
… step

README and system.md now show docker build + docker run with a ./data
bind mount as the canonical start command; docker-compose.yml is removed
(with its validity test and the js-yaml devDependencies). The dev loop
assumes an already-logged-in apify-cli — the runtime maps any non-empty
token to its single local user, per requirements/cli.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
With concurrency above 1 the autoscaled pool starts extra requests
before the max-requests stop lands and lets in-flight requests finish,
overshooting maxPages (the Python sample produced 8 items for maxPages=2
in CI). Both samples now crawl with concurrency 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
Stock apify-cli prints links using console.apify.com's path shapes
(/actors/:id/runs/:runId, /actors/:id/builds/:buildNumber,
/storage/{datasets,key-value-stores,request-queues}/:id); those now 302
to this console's own pages, and storage consoleUrl fields use the
platform's /storage/... shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
A background write landing mid-removal recreates entries under an
already-emptied directory (CI-only ENOTEMPTY flake); fs.rm's built-in
retry absorbs it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
The tight-loop test scheduled a timer that could fire before the stub
driver's resolver was assigned on slow runners; it now synchronizes on
the driver's own started signal and clears timers per iteration. Server
lifecycles are wrapped in finally so one failure can't poison the
process-wide storage singleton for later tests, and the log-drain test
polls for RUNNING instead of asserting it at a fixed instant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
…apify api covered

- The e2e suite now fails loudly when no Docker daemon is reachable
  instead of skipping, per requirements/test.md.
- apify login is no longer invoked: the suite seeds the CLI's on-disk
  credential store directly (APIFY_TOKEN env alone only authenticates
  actor:*/mcp commands in apify-cli 1.8.0, verified in its source).
- Added an e2e case driving apify api against the run and its dataset;
  verified live against the runtime without Docker first.
- Prettier pass over the requirements files edited upstream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
On first authenticated contact the runtime resolves the CLI's token
against api.apify.com/v2/users/me (3s timeout, cached per token) and
adopts the real id, username, and proxy password for its default user;
offline or with an unresolvable token it keeps the local identity.
users/me now emits proxy.password only when a real value is known
(runtime env wins over the harvested one) — the hardcoded placeholder
that could overwrite a logged-in CLI's stored proxy password is gone.
The e2e authenticates with a genuine apify login inside an isolated
HOME instead of writing the credential file by hand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
Any previously-unseen token now mints its own user on first use: identity
is harvested from the real Apify API when the token resolves there
(username, id, proxy password), and fabricated (local-user-{n}) otherwise.
The realId/realUsername overlay is gone - a user record is just what the
API serves. GET /users/:userId returns the full self view only for the
caller's own id and a minimal {id, username} for anyone else; actors,
builds, runs, and storages are owned by and filtered to their creating
user, while the console lists resources across all users with their owner.
No user is created at startup any more, and requirements files are aligned
(plus formatting fixes that were failing CI).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
…sole

The run detail page (and the runs list's dataset column) rendered
defaultDatasetId/defaultKeyValueStoreId/defaultRequestQueueId as plain
text; they now link to /datasets/:id, /key-value-stores/:id, and
/request-queues/:id respectively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FP1P3RXwZSnTxy634oWCE
@Pijukatel
Pijukatel merged commit 434e940 into v2-poc-requirements Aug 18, 2026
2 checks passed
@Pijukatel
Pijukatel deleted the claude/shepherd-local-actor-runtime-n0jcl0 branch August 18, 2026 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants