Security & Hardening
JobCtrl is local-first software with one high-risk capability: it can drive a browser or Gmail connector to submit a real application. Security therefore depends on both local data protection and binding controls around outbound actions.
This page owns the user-facing enforcement model. The complete local-file inventory is in Data, Privacy & Safety; employer-facing setup is in Apply, discovery access policy is in Discovery, and shared provider/spend controls are in Configuration. The repo threat model is in Threat Model & Security Engineering.
Implemented Hardening
JobCtrl uses defense in depth: the model prompt, UI, API, worker, browser, local tools, persistence layer, and release path each enforce a narrower part of the security contract. No single prompt or checkbox is treated as the security boundary.
- Local API & process boundaryLoopback host and peer checks, trusted browser origins, scoped local capability tokens, and worker readiness checks control access to local automation.Requests and workers
- Outbound & sensitive-tool bindingPublic HTTP(S) validation plus origin-bound credential typing and artifact upload contain untrusted pages and model output.Destinations and tools
- Credential & runtime isolationSecrets stay outside SQLite and config; JobCtrl-owned state, filtered environments, and restricted tool and file surfaces isolate execution.Secrets and model runtimes
- Approval & submission controlsBrowser-enforced dry-run, exact approval binding, durable submit intent, atomic claiming, and an at-most-once attempt control submission.Employer-facing action
- Audit & release boundaryProvenance and events preserve decisions; ambiguous submits park for verification; manifests, privacy scans, signing, and notarization gate releases.Evidence and shipped code
| Boundary | Enforced protection |
|---|---|
| Local API | Loopback bind, loopback Host and peer-address checks, first-party mutation origins, Fetch Metadata validation, and a worker-readiness gate. |
| Browser extension | Rotatable local capability token, a bounded memory-only Discovery task bridge, same-origin-only token management, a whitelisted autofill DTO, and no submit capability. |
| Outbound browsing | Public HTTP(S)-only validation for explicit task/top-level targets and each final URL; paths with an owned interception guard also validate their redirects, popups, and subrequests. Normal subresource loading in the user's Chrome profile remains Chrome/site-owned. |
| Credentials and files | Secrets stay outside SQLite and config.json; status APIs are secret-free; saved passwords and reviewed artifacts can be used only by origin-bound local tools. |
| Provider runtimes | JobCtrl-owned provider state, filtered subprocess environments, restricted tool surfaces, and prompt-driven filesystem boundaries isolate model execution from unrelated local data. |
| Application submission | Model-driven browsers are transport-locked and require manual final submit. The owned Gmail sender requires exact recipient/attachment approval, records durable submit intent immediately before send, and retains at-most-once claiming and needs_verification recovery. |
| Generated content | Structured outputs, literal evidence grounding, fabrication checks, provenance, judge review, and preservation of the last accepted artifact. |
| Bundled runtime | Payload path confinement, isolated Python startup, manifest verification, and hash-locked provider packs with activation-time revalidation. |
| Privacy and release | Metadata-only telemetry, synthetic-demo isolation, private local file modes for sensitive control files, and release scanning for secrets and private artifacts. |
Public Demo Boundary
The hosted public demo uses synthetic data in a browser-local workspace. Its shortcuts and actions are simulations: they do not contact employers, send messages, or make external changes.
For the documentation site's optional analytics consent and the demo's separate analytics-cookie gate, Google Analytics scope, retention, data-controller identity, and privacy contact, read the canonical Data, Privacy & Safety notice.
What Leaves Your Machine
| Outbound call | When | What can be sent |
|---|---|---|
| LLM providers | Scoring, analysis, tailoring, cover letters, contact extraction, opt-in Required-bullet coaching | Posting text, relevant profile evidence, generated text, or opted-in page text. Required-bullet coaching sends only selected saved claims, role/company context and matching achievement evidence. Employer analysis uses Claude, Codex, and Gemini legs. |
| Apply model | Apply/dry-run or an enabled standing loop | The reviewed application URL and page content observed during the transport-locked inspection. Stored profile fields, job-description prose, tailored resume text, cover-letter text, local artifact paths, and artifact-upload authority are excluded. |
| Job/ATS/public pages | Integrated Discovery and its enrichment drain through the user's current Chrome profile; supervised contact research through its separate gateway | Search terms and network/page requests. During Discovery, Chrome may also send that site's existing cookies/session state; those browser-owned values are not copied into worker tasks. |
| Salary data sources | At the end of a Discover run, only for benchmark slices that are missing or past their seven-day freshness window; Levels.fyi or Glassdoor only when their Settings policy permits | Requests for public benchmark data from Euro Top Tech, ECB exchange rates when a currency conversion is needed, and Eurostat price levels when a slice has no direct benchmark. Levels.fyi public-page URLs name the role and location being benchmarked. |
| Gmail | Authenticated verification/outcome flows or approved email applications | Bounded queries/evidence, or the exact reviewed send. |
| Google Maps | Configured profile autocomplete | Address text typed in the field. |
| CAPTCHA provider | Supported widget during an apply run you explicitly start or a standing loop you enable, with a configured solver | Site key and page URL through the local solver tool. |
| Langfuse/OpenTelemetry | Explicit telemetry configuration | Metadata-only provider/model, operation/stage, outcome, token-count, and safe-size span attributes. |
Discovery and Enrich snapshots contain HTML and text rendered in your signed-in Chrome session, which can include personalized account content beyond the job posting. Extracted posting text can be stored locally, and snapshots or posting text can reach configured LLM providers during extraction and later stages. See the data-flow notice. Cookie values are never copied into worker tasks or results; the browser user-agent string is returned with results as transport metadata; it is not used for robots checks.
The Apply model receives no applicant profile or generated-material prose. Reviewed resume and cover-letter files remain local for the user to handle manually; the page-reading model cannot upload or access them.
What Stays On Your Machine
The local authorities are jobctrl.db (profile, jobs, Discovery controls, events and projections), bundled temporal.db, config.json (non-secret Settings values), .env and gmail/ (legacy/runtime credentials), codex_home/ and provider-runtime directories, generated material/log directories, browser capability/profile/ worker state, backups/, and legacy resume inputs. Their text may still be sent when an explicitly used model or external integration needs that content; configured telemetry remains metadata-only.
Browser-adoption metadata is stored in config.json, while the extension pairing token remains a separate protected artifact. An installation upgraded from an older release may still contain a previously consented copied profile, but current Settings neither creates nor selects it for LinkedIn work. Integrated Discovery and Enrich use the extension in the current Chrome profile and never read those copies. Discovery task bodies and results live only in the API broker's process memory while the bounded task is active.
Local does not mean encrypted
JobCtrl does not encrypt jobctrl.db, .env, or generated artifacts. Protect your OS account/disk and never attach ~/.jobctrl/ to a bug report.
Local API And Process Boundary
The product API binds to 127.0.0.1 by default. Every request must name a loopback Host and arrive from a loopback peer address. Checking both prevents a remote caller from passing the gate merely by forging a Host: 127.0.0.1 header and limits DNS-rebinding attacks from browser pages.
State-changing browser requests must also come from the first-party local web ports and carry a trusted Origin or Referer plus acceptable Sec-Fetch-Site metadata. Non-browser local clients need the local capability token instead. Worker-backed actions fail with worker_runtime_unavailable until the API sees a healthy worker heartbeat, so a dead or mismatched worker is not mistaken for an executable product state.
These controls are locality enforcement, not user authentication. Deliberately binding the API to a non-loopback interface changes the threat model and is not a supported substitute for hosted authentication, tenant authorization, and a managed secret store.
Browser Extension Pairing
The extension uses a local capability token for /v1/extension/* loopback routes. Settings can display/rotate it only from the CLI or same-origin Settings surface. The token does not make a remote API bind safe.
The app directory is created with owner-only permissions and the token file is written with mode 0600. Rotation replaces the token immediately and disconnects existing extension clients. An extension-origin request can use a valid token for its scoped routes, but it cannot read, mint, or rotate the token itself.
Once paired, the extension polls a loopback-only lease route and supplies the heartbeat shown in Settings. When selected for acquisition, the extension receives only execution-bound, idempotent tasks carrying the exact Temporal workflow/run identity, one public HTTP(S) URL, a GET or POST request or a rendered-page request, a maximum two-minute timeout, and bounded bodies/results. Browser-owned Cookie, User-Agent, Origin, Referer, Host, and all Sec-*/Proxy-* headers are rejected at the contract and stripped again by the worker/content script; Chrome supplies its own values from the current profile. Saving the token in the extension explicitly binds one UUID-backed extension installation; the binding is stored outside Chrome as a mode-0600 control file, Settings exposes only its last eight characters, and another Chrome profile cannot heartbeat, lease, or complete work until the user replaces the selection there. The broker is memory-only and admits at most four active/pending tasks for four extension executors. Queue waiting and lease execution use separate bounds. Each active lease heartbeats while Chrome works; cancellation or a hard timeout aborts the request, closes its owned tab when one exists, and invalidates stale completion. A selected extension disconnect can fail its acquisition; it does not trigger another transport for that fetch. The next acquisition setup may select anonymous access if the bounded status probe reports unavailable.
Capture requires a click and sends the active page URL/visible text to the local API. If the stack is down, the extension holds a bounded local queue. Autofill has page access on ordinary HTTP(S) sites so it is not coupled to a fixed ATS host list, but its content script stays passive until Review autofill is clicked. Only then does it read the current form and receive whitelisted profile fields from the loopback API. It shows a review panel and never calls submit. Passwords, resume content, and generated free-text answers are excluded.
Outbound Destination And Sensitive-Tool Binding
JobCtrl treats URLs from postings, redirects, captured pages, and model output as untrusted. Its shared public-destination guard resolves the target and accepts only public HTTP(S). It blocks loopback, private-network, link-local, reserved, unspecified, multicast, cloud-metadata, file:, and other non-web targets. For connected-extension acquisition, the API validates each task's initial URL before leasing it and revalidates DNS immediately before returning the lease. The extension's wildcard HTTP(S) host permission exists only to execute those brokered tasks in the installed profile. Direct HTTP/API tasks run in the service worker with redirect following disabled. Rendered-page navigation uses a tab-scoped exact-origin allow rule above a default HTTP(S) block rule for main-frame navigation only. A cross-origin redirect is therefore blocked before its destination request is sent. The extension observes navigation errors only for its temporary task tab, using the webNavigation permission, and reports a blocked cross-origin navigation as non-retryable unsafe_redirect immediately. The content script and broker also reject non-web, credential-bearing, lexically local, or cross-origin final URLs. A rendered page's ordinary subresource loading remains owned by Chrome and the visited site, as it would during normal browsing in that profile. Anonymous JobStreaming uses guarded Requests sessions for every provider, including providers normally backed by native tls-client. URL/DNS checks run before each send, including automatic redirects; direct sockets connect to validated numeric public addresses. Search-session replacement and per-detail sessions use the same guard. Public redirects preserve normal Requests handling, including removal of cross-origin credentials. Proxy routing is rejected in this anonymous broad-board path because proxy-side destination DNS cannot be pinned by the worker; it never silently ignores a configured proxy. Provider headers, cookies, request bodies and timeout options remain supported. ATS/API and Workday use the existing guarded HTTP gateway; anonymous Smart Extract and Enrich use the public Playwright route guard described below. Contact research and Apply retain their own stricter transport guards.
For public-page rendering, JobCtrl replays only safe public GET and HEAD requests through its DNS-pinned fetcher. It drops public write subrequests such as analytics or telemetry POSTs without replaying them and without rejecting the otherwise safe page. A private, loopback, metadata, rebinding, or otherwise non-public destination remains fatal for the whole extraction.
Fetch failures retain a typed cause and observation time. A timeout, connection failure, or unavailable DNS lookup can retry within the normal attempt limit. A hostname that resolved to a non-public address stays blocked for that attempt. While the worker is idle, eligible saved failures receive at most five DNS-only rechecks with backoff. Both the original posting and the recorded failed request must currently validate as public before normal guarded enrichment can resume. Literal non-public addresses, invalid URLs, TLS failures, response-size limits, and unknown safety failures do not get this recovery. A new request still runs every destination, redirect, DNS-pinning, and read-only method guard. Existing attempts and failure events remain in the audit history.
Apply adds a stricter browser capability: every intercepted HTTP(S) request from its owned Chrome page targets must remain on the canonical origin of the approved application URL. The check is enforced in the browser-level CDP guard for the initial page, redirects, popups, tabs, form submissions, and subresources. Dedicated, shared, and service-worker targets are closed before their code runs because Chrome does not expose the required request-interception domain on those targets. A page or model cannot grant itself a second public origin; an unexpected cross-origin ATS transition stops the run and needs a newly reviewed application destination.
The page-reading model receives no generic text-entry, form-fill, keypress, select, drag, dialog-response, saved-credential, Gmail verification-code, or artifact-upload capability. The default MCP configuration does not start the Gmail connector or pass the profile database and credential-origin policy to the inspection agent. These tools are also explicitly denied even if a caller supplies a broader MCP configuration.
Reviewed artifacts are not staged in the browser worker, preventing a same-origin page from reflecting their contents back through model-visible DOM.
Approval And Control Gates
Apply Approval Is Required By Default
With applyApprovalRequired: true, the latest decision must be approve_submit and bind the current materials generation, profile version, application URL, and qualifying dry-run evidence. A partial rehearsal requires an explicit approval for that run and its blocked channels.
The Python launcher's atomic claim checks the binding. This is not a UI-only warning, so API/RPC dispatch cannot bypass it while enabled. Turning the setting off removes the claim-time approval wait, but does not grant final browser-submit authority or bypass a sink-specific approval check.
Email-only applications use the same binding for the exact recipient and resume attachment. Missing scope, sender, or exact approval fails closed.
Repeat Applications Fail Closed
Every live claim also re-evaluates repeat-application evidence at the Python worker boundary. A confirmed application to the same canonical job or an accepted duplicate identity blocks by default. A confirmed application to the same employer and a conservatively equivalent role requires explicit confirmation. Distinct roles and merely similar employer names remain eligible.
The evidence can come only from canonical job identity and confirmed application facts. Pending email suggestions, notes, dry runs, failed pre-submit attempts, submit intent without confirmation, and assumptions are excluded. Apply Review exposes the prior application, relationship reason, identity evidence, and audit trail rather than presenting an unexplained warning.
An override records the intended target, selected prior application, current evidence fingerprint, actor, reason, and timestamp. The worker consumes it for one run while holding the same BEGIN IMMEDIATE claim transaction; a stale or already-consumed override fails closed. This guard applies even when per-job approval is disabled and cannot be bypassed by a direct API/RPC request, the standing loop, a stale UI, or concurrent claims. Approval binding, submit intent, at-most-once claiming, and needs_verification remain separate required gates.
Dry-Run Cannot Submit
Dry-run combines instruction with enforcement. A Chrome DevTools Protocol guard grants one exact GET to the reviewed application URL and records the sanitized decision plus a URL fingerprint. It blocks HEAD, replay, path or query changes, redirects, later document navigation, mutating requests, form submission, WebSocket/beacon channels, and data-bearing subresource exfiltration. A receipt cannot claim full coverage unless that owned initial grant was consumed and recorded.
The agent's status is accepted only from one successful dedicated terminal field matching an exact result grammar. Error envelopes, narration, page-influenced prose, missing records, multiple records, and records with extra text remain audit output and cannot select a success state. A model-reported RESULT:DRY_RUN is also marked semantic_review_unverified and remains partial evidence; accepting it requires the explicit partial-rehearsal approval described above. A model-reported RESULT:APPLIED is never an authoritative submission receipt.
Because the browser enforces the boundary, safety does not depend on the agent choosing not to click Submit.
Browser Capabilities Are Explicit
Discovery and Enrich prefer the selected paired extension when its bounded status probe reports connected. Otherwise they use guarded public HTTP or anonymous managed Playwright. API launch and job-level/bulk retries remain available offline, subject to worker readiness, authorization and stage gates. A failed acquisition or cancellation never switches transports. Neither mode adopts a Chrome executable or reads a copied profile, even if a standalone compatibility capability has prior consent. The optional Apply browser remains disabled until explicitly adopted.
The Apply-browser Settings read path may detect supported Chrome/Chromium installations, but it returns only a bounded ID and label. Detection never launches, adopts, copies, or persists a browser and never returns its path. Adoption is a separate explicit mutation selecting either the current detected ID or a write-only executable path. Detected IDs are revalidated at adoption time; a stale ID fails closed rather than silently selecting another installation.
Browser Final Submit Is Not Delegated
The page-reading model receives an inspection-only prompt containing the reviewed application URL. It never receives profile, resume, cover-letter, job-description, generated-material prose, local artifact paths, or artifact-upload authority. It can inspect a transport-locked rehearsal, but it cannot perform the final browser submit. A live browser-form run stops with trusted_final_submit_required before prompt rendering, browser launch, or agent execution. The same fail-closed rule is enforced by the use case, saga, and agent adapter, so a direct internal call cannot recover generic browser submit authority.
The remaining automated live sink is JobCtrl's owned Gmail sender. An apply run cannot claim a job already running, succeeded, or awaiting verification. The sender rechecks its capability and records durable submit intent immediately before sending the exact approved recipient/attachment candidate. If the process dies or the provider raises after that checkpoint without a confirmed terminal result, JobCtrl parks the job in needs_verification instead of retrying.
Daily LLM Spend Ceiling
dailyBudgetUsd defaults to 25; 0 means unlimited. Spendful workflows run a preflight and stop non-retryably after the daily estimate reaches the limit. It is a coarse workflow-start guard, not provider billing truth or a mid-call kill switch.
No Third-Party Bypass
JobCtrl does not bypass login, paywall, rate-limit, bot-control, identity, biometric, payment, or bank-detail gates. The apply agent declines browser permission prompts and stops on SSO/OAuth or unsupported CAPTCHA. Supported CAPTCHA solving uses only the owned local tool; provider keys/tokens never enter the model prompt. Setting CAPSOLVER_API_KEY is explicit authority to send the supported widget's site key and page URL during apply work you start or a standing loop you enable. The local solver permits one outbound provider attempt per apply run; a failed attempt is not retried. Without that authority, or for unsupported/failed challenges, the apply path stops.
Crawl Politeness
Discovery and Enrich do not request, evaluate, or enforce robots.txt in connected or anonymous mode. Legacy blocked outcomes remain readable and retryable; old source-policy values cannot reactivate enforcement.
- Initial URLs, DNS resolution, redirects and final URLs retain their public destination checks; rendered browser tasks retain exact-origin navigation guards.
- Per-host pacing, concurrency, and per-run request budgets limit load.
- Chrome owns its cookies, proxy and user agent; anonymous acquisition never borrows a personal profile or ambient credentials.
- Rate-limit, budget and unsafe-destination outcomes remain distinguishable from generic scrape errors, and cancellation still ends owned work.
JobStreaming owns internal board traversal. JobCtrl applies invocation-level pacing/budget there, with a public-destination guard on every anonymous request, rather than claiming per-request budget accounting. Its provider sessions prefer the extension when connected. The configured JobCtrl/<version> (+<contact>) identity remains in use for non-extension gateways and integrated anonymous broad-board requests; it is never substituted for Chrome's live user agent. Copied-profile capabilities remain separate. See Discovery → Crawl Politeness.
Credentials
Codex-backed work requires an authenticated Codex CLI before JobCtrl can reuse or verify that login.
| Secret | Boundary |
|---|---|
| Provider/runtime keys | Native OS credential store for guided persistent setup; inherited environment for ephemeral use; legacy .env until migrated. Never SQLite. |
| Codex login | Stable $JOBCTRL_DIR/codex_home/auth.json, separate from SQLite and the normal Codex home. Valid normal CLI auth may be reused once only when this file is absent; only auth.json is copied, and existing isolated auth is never overwritten. Generation chats are ephemeral and app-server databases/logs are temporary for the pooled client lifetime. Prompt-driven reads are limited to the private project rooted at codex_home/workspace/ plus the exact canonical Codex executable required to start the app server—not its package directory or sibling files. Configuration discovery stops at that generated project boundary. |
| Claude/Google web entries | Native OS credential store for API keys; config.json for cloud activation flags/non-secret identifiers; AWS, Google, and Azure credential files remain in their vendor stores. Status only is returned. |
| CAPTCHA key | CAPSOLVER_API_KEY saved from Settings to the native OS credential store, or supplied by the environment; read by the owned local solver, not the model. |
| Job-site passwords | Optional local profile value typed through a focused-field credential tool, never returned to the model. |
Codex token refresh and revocation use OpenAI's default HTTPS endpoints; inherited endpoint overrides cannot redirect those credentials elsewhere.
config.json contains non-secret Settings values and is replaced atomically with owner-only mode 0600. The extension capability token is also 0600. This limits accidental access by other local accounts, but it is not encryption and does not protect against a compromised user account.
Environment values win over the OS credential store and make the matching Settings control read-only. Native credentials are loaded only for missing/empty values at process startup, so restart the relevant Python process after Claude, Google, or CapSolver changes. Codex verification, model preference, browser capability, and extension-pairing changes do not require that restart. Provider replacement is atomic from the web contract; native reads for presence, rollback, or migration stay internal and never cross the HTTP boundary. Windows uses Credential Manager and Linux uses Secret Service. Unavailable or locked stores are reported without falling back to plaintext persistence.
Runtime And Supply-Chain Boundary
Bundled mode fails closed around its installed payload. The launcher supplies an absolute payload root; the API resolves its Python, web, and browser paths and rejects paths outside that tree. Python starts in isolated mode, ignores ambient PYTHONHOME, PYTHONPATH, virtual environments, and user-site packages, and does not search a checkout for dotenv files.
Provider runtimes that cannot ship in the core payload are installed from the payload-owned provider-pack lock. JobCtrl accepts only the exact locked wheel closure from the official HTTPS wheel host, checks size and SHA-256, rejects unsafe, encrypted, escaping, or overlapping archive members, and retains the source wheels. Activation revalidates those bytes, deterministically derives the expected installed tree, and compares it with the live provider pack before executing it.
The native supervisor verifies the payload manifest and tree before dispatch and records process identity, executable, build identity, manifest digest, and ports so cleanup cannot target a recycled PID. Promotion of public bundled artifacts also requires the repository privacy scan, signing, and notarization; the scan rejects secret assignments, private-profile needles, browser profiles, databases, resumes, tokens, logs, and other blocked user artifacts.
The Apply Agent
The local Claude runtime drives Chrome through an explicit apply-tool allowlist. It has inspection navigation, snapshots/screenshots, and a bounded CAPTCHA capability—not generic form-entry, credential, verification-code, artifact, shell/file, raw mailbox/send, broad permission-bypass, or arbitrary page-script tools. Owned JobCtrl code performs approved email application sends.
Prompt injection remains a real risk
The agent reads untrusted pages that can attempt to manipulate it. The controls limit consequences; they cannot make untrusted page content safe.
The practical containment layers are a transport-locked browser, browser-enforced dry-run, manual final browser submit, exact approval for the owned email sender, at-most-once owned sends, spend limits, local credential tools, and hard prompt rules against fabricated answers, permissions, or sensitive payments. If a legal/screening attestation is missing, the agent stops instead of guessing.
Content Integrity And Auditability
Job postings, imported documents, page text and model responses are untrusted inputs. Semantic claim support comes from a strict model determination. Code checks canonical IDs, exact verbatim citations, numeric/date/currency equality, schemas and versions before an artifact is accepted. An independent quality judge remains separate. Generated lines record source and requirement anchors; read-side code cannot infer provenance from text overlap. Model failure has a distinct safe status and never selects a lexical fallback.
Bounded verifier findings can request repair while remaining untrusted source data. They cannot add a profile fact, change the allowlisted source inventory, override a user pin or bypass the structural gates.
When regeneration or review fails, JobCtrl keeps the last accepted artifact visible and records the failed attempt at a separate audit path instead of overwriting, replacing, or hiding known-good material. Approval decisions, including successful refreshes, use immutable artifact paths so a persistence failure cannot change the bytes behind the current approved record. Replacing approved text also invalidates its derived PDF before persistence, so a render failure cannot leave a stale PDF marked approved. Submit intent, provider failures, residual warnings, terminal outcomes, and verification parks remain inspectable in the local event/read model so the UI can explain what happened and which evidence was used.
Scoring Is Applicant-Side Only
Fit scores help the applicant prioritize. They are not employer-side screening and must not rank people for hiring without separate legal, bias-audit, validation, notice, and human-review controls.
Reporting A Security Issue
Prefer GitHub private vulnerability reporting. Otherwise request a private contact path in a minimal public issue; omit exploit details, secrets, logs, profiles, artifacts, and local paths. See SECURITY.md.
Semantic Determination Authority
Semantic decisions come from strict model determinations with source citations. Code owns ID membership, verbatim spans, exact values, version fences, URL/security controls and privacy minimization. No provider failure selects a lexical substitute. Form mapping uses captured questions, saved fact IDs and native option IDs; the user confirms filling and JobCtrl never submits.
See the decision for caching, provenance and failure contracts.